mirror of https://github.com/JohnHammond/CTFd.git
CTFd code push
parent
20183dd3c9
commit
376c90189b
|
@ -52,3 +52,6 @@ docs/_build/
|
|||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
*.db
|
||||
*.log
|
|
@ -0,0 +1,76 @@
|
|||
from flask import Flask, render_template, request, redirect, abort, session, jsonify, json as json_mod, url_for
|
||||
from flask.ext.sqlalchemy import SQLAlchemy
|
||||
from flask.ext.mail import Mail, Message
|
||||
from logging.handlers import RotatingFileHandler
|
||||
from flask.ext.session import Session
|
||||
import logging
|
||||
import os
|
||||
import sqlalchemy
|
||||
|
||||
def create_app(subdomain, username="", password=""):
|
||||
app = Flask("CTFd", static_folder="../static", template_folder="../templates")
|
||||
with app.app_context():
|
||||
app.config.from_object('CTFd.config')
|
||||
|
||||
if subdomain:
|
||||
app.config.update(
|
||||
SQLALCHEMY_DATABASE_URI = 'mysql://'+username+':'+password+'@localhost:3306/' + subdomain + '_ctfd',
|
||||
HOST = subdomain + app.config["HOST"],
|
||||
SESSION_FILE_DIR = app.config['SESSION_FILE_DIR'] + "/" + subdomain,
|
||||
DEBUG = True
|
||||
)
|
||||
|
||||
from CTFd.models import db, Teams, Solves, Challenges, WrongKeys, Keys, Tags, Files, Tracking
|
||||
|
||||
db.init_app(app)
|
||||
db.create_all()
|
||||
|
||||
app.db = db
|
||||
# app.setup = True
|
||||
|
||||
mail = Mail(app)
|
||||
|
||||
Session(app)
|
||||
|
||||
from CTFd.views import init_views
|
||||
init_views(app)
|
||||
from CTFd.errors import init_errors
|
||||
init_errors(app)
|
||||
from CTFd.challenges import init_challenges
|
||||
init_challenges(app)
|
||||
from CTFd.scoreboard import init_scoreboard
|
||||
init_scoreboard(app)
|
||||
from CTFd.auth import init_auth
|
||||
init_auth(app)
|
||||
from CTFd.admin import init_admin
|
||||
init_admin(app)
|
||||
from CTFd.utils import init_utils
|
||||
init_utils(app)
|
||||
|
||||
return app
|
||||
|
||||
|
||||
# logger_keys = logging.getLogger('keys')
|
||||
# logger_logins = logging.getLogger('logins')
|
||||
# logger_regs = logging.getLogger('regs')
|
||||
|
||||
# logger_keys.setLevel(logging.INFO)
|
||||
# logger_logins.setLevel(logging.INFO)
|
||||
# logger_regs.setLevel(logging.INFO)
|
||||
|
||||
# try:
|
||||
# parent = os.path.dirname(__file__)
|
||||
# except:
|
||||
# parent = os.path.dirname(os.path.realpath(sys.argv[0]))
|
||||
|
||||
# key_log = RotatingFileHandler(os.path.join(parent, 'logs', 'keys.log'), maxBytes=10000)
|
||||
# login_log = RotatingFileHandler(os.path.join(parent, 'logs', 'logins.log'), maxBytes=10000)
|
||||
# register_log = RotatingFileHandler(os.path.join(parent, 'logs', 'registers.log'), maxBytes=10000)
|
||||
|
||||
# logger_keys.addHandler(key_log)
|
||||
# logger_logins.addHandler(login_log)
|
||||
# logger_regs.addHandler(register_log)
|
||||
|
||||
# logger_keys.propagate = 0
|
||||
# logger_logins.propagate = 0
|
||||
# logger_regs.propagate = 0
|
|
@ -0,0 +1,397 @@
|
|||
from CTFd import render_template, request, redirect, abort, jsonify, url_for, session
|
||||
from CTFd.utils import sha512, is_safe_url, authed, admins_only, is_admin, unix_time, unix_time_millis
|
||||
from CTFd.models import db, Teams, Solves, Challenges, WrongKeys, Keys, Tags, Files, Tracking, Pages, Config
|
||||
from itsdangerous import TimedSerializer, BadTimeSignature
|
||||
from werkzeug.utils import secure_filename
|
||||
from socket import inet_aton, inet_ntoa
|
||||
from passlib.hash import bcrypt_sha256
|
||||
from flask import current_app as app
|
||||
|
||||
import logging
|
||||
import hashlib
|
||||
import time
|
||||
import re
|
||||
import os
|
||||
import json
|
||||
|
||||
def init_admin(app):
|
||||
@app.route('/admin', methods=['GET', 'POST'])
|
||||
def admin():
|
||||
if request.method == 'POST':
|
||||
username = request.form.get('name')
|
||||
password = request.form.get('password')
|
||||
|
||||
admin = Teams.query.filter_by(name=request.form['name'], admin=True).first()
|
||||
if admin and bcrypt_sha256.verify(request.form['password'], admin.password):
|
||||
session.regenerate() # NO SESSION FIXATION FOR YOU
|
||||
session['username'] = admin.name
|
||||
session['id'] = admin.id
|
||||
session['admin'] = True
|
||||
session['nonce'] = sha512(os.urandom(10))
|
||||
db.session.close()
|
||||
return redirect('/admin/graphs')
|
||||
|
||||
if is_admin():
|
||||
return redirect('/admin/graphs')
|
||||
|
||||
return render_template('admin/login.html')
|
||||
|
||||
@app.route('/admin/graphs')
|
||||
@admins_only
|
||||
def admin_graphs():
|
||||
return render_template('admin/graphs.html')
|
||||
|
||||
@app.route('/admin/config', methods=['GET', 'POST'])
|
||||
@admins_only
|
||||
def admin_config():
|
||||
if request.method == "POST":
|
||||
start = request.form['start']
|
||||
end = request.form['end']
|
||||
|
||||
if not start:
|
||||
start = None
|
||||
else:
|
||||
start = int(start)
|
||||
if not end:
|
||||
end = None
|
||||
else:
|
||||
end = int(end)
|
||||
|
||||
print repr(start), repr(end)
|
||||
|
||||
db_start = Config.query.filter_by(key='start').first()
|
||||
db_start.value = start
|
||||
|
||||
db_end = Config.query.filter_by(key='end').first()
|
||||
db_end.value = end
|
||||
|
||||
db.session.add(db_start)
|
||||
db.session.add(db_end)
|
||||
|
||||
db.session.commit()
|
||||
return redirect('/admin/config')
|
||||
start = Config.query.filter_by(key="start").first().value
|
||||
end = Config.query.filter_by(key="end").first().value
|
||||
return render_template('admin/config.html', start=start, end=end)
|
||||
|
||||
@app.route('/admin/pages', defaults={'route': None}, methods=['GET', 'POST'])
|
||||
@app.route('/admin/pages/<route>', methods=['GET', 'POST'])
|
||||
@admins_only
|
||||
def admin_pages(route):
|
||||
if route and request.method == 'GET':
|
||||
page = Pages.query.filter_by(route=route).first()
|
||||
return render_template('admin/editor.html', page=page)
|
||||
if route and request.method == 'POST':
|
||||
page = Pages.query.filter_by(route=route).first()
|
||||
errors = []
|
||||
html = request.form['html']
|
||||
route = request.form['route']
|
||||
if not route:
|
||||
errors.append('Missing URL route')
|
||||
if errors:
|
||||
page = Pages(html, "")
|
||||
return render_template('/admin/editor.html', page=page)
|
||||
if page:
|
||||
page.route = route
|
||||
page.html = html
|
||||
db.session.commit()
|
||||
return redirect('/admin/pages')
|
||||
page = Pages(route, html)
|
||||
db.session.add(page)
|
||||
db.session.commit()
|
||||
return redirect('/admin/pages')
|
||||
if not route and request.method == 'POST':
|
||||
return render_template('admin/editor.html')
|
||||
pages = Pages.query.all()
|
||||
return render_template('admin/pages.html', routes=pages)
|
||||
|
||||
|
||||
@app.route('/admin/chals', methods=['POST', 'GET'])
|
||||
@admins_only
|
||||
def admin_chals():
|
||||
# if authed():
|
||||
if request.method == 'POST':
|
||||
chals = Challenges.query.add_columns('id', 'name', 'value', 'description', 'category').order_by(Challenges.value).all()
|
||||
|
||||
json = {'game':[]}
|
||||
for x in chals:
|
||||
json['game'].append({'id':x[1], 'name':x[2], 'value':x[3], 'description':x[4], 'category':x[5]})
|
||||
|
||||
db.session.close()
|
||||
return jsonify(json)
|
||||
else:
|
||||
return render_template('admin/chals.html')
|
||||
|
||||
@app.route('/admin/keys/<chalid>', methods=['POST', 'GET'])
|
||||
@admins_only
|
||||
def admin_keys(chalid):
|
||||
if request.method == 'GET':
|
||||
keys = Keys.query.filter_by(chal=chalid).all()
|
||||
json = {'keys':[]}
|
||||
for x in keys:
|
||||
json['keys'].append({'id':x.id, 'key':x.flag, 'type':x.key_type})
|
||||
return jsonify(json)
|
||||
elif request.method == 'POST':
|
||||
keys = Keys.query.filter_by(chal=chalid).all()
|
||||
for x in keys:
|
||||
db.session.delete(x)
|
||||
|
||||
newkeys = request.form.getlist('keys[]')
|
||||
newvals = request.form.getlist('vals[]')
|
||||
for flag, val in zip(newkeys, newvals):
|
||||
key = Keys(chalid, flag, val)
|
||||
db.session.add(key)
|
||||
|
||||
db.session.commit()
|
||||
db.session.close()
|
||||
return '1'
|
||||
|
||||
@app.route('/admin/tags/<chalid>', methods=['GET', 'POST'])
|
||||
@admins_only
|
||||
def admin_tags(chalid):
|
||||
if request.method == 'GET':
|
||||
tags = Tags.query.filter_by(chal=chalid).all()
|
||||
json = {'tags':[]}
|
||||
for x in tags:
|
||||
json['tags'].append({'id':x.id, 'chal':x.chal, 'tag':x.tag})
|
||||
return jsonify(json)
|
||||
|
||||
elif request.method == 'POST':
|
||||
newtags = request.form.getlist('tags[]')
|
||||
for x in newtags:
|
||||
tag = Tags(chalid, x)
|
||||
db.session.add(tag)
|
||||
db.session.commit()
|
||||
db.session.close()
|
||||
return '1'
|
||||
|
||||
@app.route('/admin/tags/<tagid>/delete', methods=['POST'])
|
||||
@admins_only
|
||||
def admin_delete_tags(tagid):
|
||||
if request.method == 'POST':
|
||||
tag = Tags.query.filter_by(id=tagid).first_or_404()
|
||||
db.session.delete(tag)
|
||||
db.session.commit()
|
||||
db.session.close()
|
||||
return "1"
|
||||
|
||||
|
||||
@app.route('/admin/files/<chalid>', methods=['GET', 'POST'])
|
||||
@admins_only
|
||||
def admin_files(chalid):
|
||||
if request.method == 'GET':
|
||||
files = Files.query.filter_by(chal=chalid).all()
|
||||
json = {'files':[]}
|
||||
for x in files:
|
||||
json['files'].append({'id':x.id, 'file':x.location})
|
||||
return jsonify(json)
|
||||
if request.method == 'POST':
|
||||
if request.form['method'] == "delete":
|
||||
f = Files.query.filter_by(id=request.form['file']).first_or_404()
|
||||
if os.path.isfile(f.location):
|
||||
os.unlink(f.location)
|
||||
db.session.delete(f)
|
||||
db.session.commit()
|
||||
db.session.close()
|
||||
return "1"
|
||||
elif request.form['method'] == "upload":
|
||||
files = request.files.getlist('files[]')
|
||||
|
||||
for f in files:
|
||||
filename = secure_filename(f.filename)
|
||||
|
||||
if len(filename) <= 0:
|
||||
continue
|
||||
|
||||
md5hash = hashlib.md5(os.urandom(64)).hexdigest()
|
||||
|
||||
# BUG NEEDS TO GO TO S3
|
||||
if not os.path.exists(os.path.join(app.config['UPLOAD_FOLDER'], md5hash)):
|
||||
os.makedirs(os.path.join(app.config['UPLOAD_FOLDER'], md5hash))
|
||||
|
||||
f.save(os.path.join(app.config['UPLOAD_FOLDER'], md5hash, filename))
|
||||
db_f = Files(chalid, os.path.join(app.config['UPLOAD_FOLDER'], md5hash, filename))
|
||||
db.session.add(db_f)
|
||||
|
||||
db.session.commit()
|
||||
db.session.close()
|
||||
return redirect('/admin/chals')
|
||||
|
||||
@app.route('/admin/teams')
|
||||
@admins_only
|
||||
def admin_teams():
|
||||
teams = Teams.query.all()
|
||||
return render_template('admin/teams.html', teams=teams)
|
||||
|
||||
@app.route('/admin/team/<teamid>', methods=['GET', 'POST'])
|
||||
@admins_only
|
||||
def admin_team(teamid):
|
||||
user = Teams.query.filter_by(id=teamid).first()
|
||||
solves = Solves.query.filter_by(teamid=teamid).all()
|
||||
addrs = Tracking.query.filter_by(team=teamid).group_by(Tracking.ip).all()
|
||||
db.session.close()
|
||||
|
||||
if request.method == 'GET':
|
||||
return render_template('admin/team.html', solves=solves, team=user, addrs=addrs)
|
||||
elif request.method == 'POST':
|
||||
json = {'solves':[]}
|
||||
for x in solves:
|
||||
json['solves'].append({'id':x.id, 'chal':x.chalid, 'team':x.teamid})
|
||||
return jsonify(json)
|
||||
|
||||
@app.route('/admin/team/<teamid>/ban', methods=['POST'])
|
||||
@admins_only
|
||||
def ban(teamid):
|
||||
user = Teams.query.filter_by(id=teamid).first()
|
||||
user.banned = 1;
|
||||
db.session.commit()
|
||||
return redirect('/scoreboard')
|
||||
|
||||
@app.route('/admin/team/<teamid>/unban', methods=['POST'])
|
||||
@admins_only
|
||||
def unban(teamid):
|
||||
user = Teams.query.filter_by(id=teamid).first()
|
||||
user.banned = None;
|
||||
db.session.commit()
|
||||
return redirect('/scoreboard')
|
||||
|
||||
|
||||
@app.route('/admin/graphs/<graph_type>')
|
||||
@admins_only
|
||||
def admin_graph(graph_type):
|
||||
if graph_type == 'categories':
|
||||
categories = db.session.query(Challenges.category, db.func.count(Challenges.category)).group_by(Challenges.category).all()
|
||||
json = {'categories':[]}
|
||||
for category, count in categories:
|
||||
json['categories'].append({'category':category, 'count':count})
|
||||
return jsonify(json)
|
||||
elif graph_type == "solves":
|
||||
solves = Solves.query.add_columns(db.func.count(Solves.chalid)).group_by(Solves.chalid).all()
|
||||
json = {}
|
||||
for chal, count in solves:
|
||||
json[chal.chal.name] = count
|
||||
return jsonify(json)
|
||||
|
||||
@app.route('/admin/scoreboard')
|
||||
@admins_only
|
||||
def admin_scoreboard():
|
||||
score = db.func.sum(Challenges.value).label('score')
|
||||
quickest = db.func.max(Solves.date).label('quickest')
|
||||
teams = db.session.query(Solves.teamid, Teams.name, score).join(Teams).join(Challenges).filter(Teams.banned == None).group_by(Solves.teamid).order_by(score.desc(), quickest)
|
||||
db.session.close()
|
||||
return render_template('admin/scoreboard.html', teams=teams)
|
||||
|
||||
@app.route('/admin/scores')
|
||||
@admins_only
|
||||
def admin_scores():
|
||||
score = db.func.sum(Challenges.value).label('score')
|
||||
quickest = db.func.max(Solves.date).label('quickest')
|
||||
teams = db.session.query(Solves.teamid, Teams.name, score).join(Teams).join(Challenges).filter(Teams.banned == None).group_by(Solves.teamid).order_by(score.desc(), quickest)
|
||||
db.session.close()
|
||||
json = {'teams':[]}
|
||||
for i, x in enumerate(teams):
|
||||
json['teams'].append({'place':i+1, 'id':x.teamid, 'name':x.name,'score':int(x.score)})
|
||||
return jsonify(json)
|
||||
|
||||
@app.route('/admin/solves/<teamid>', methods=['GET'])
|
||||
@admins_only
|
||||
def admin_solves(teamid="all"):
|
||||
if teamid == "all":
|
||||
solves = Solves.query.all()
|
||||
else:
|
||||
solves = Solves.query.filter_by(teamid=teamid).all()
|
||||
db.session.close()
|
||||
json = {'solves':[]}
|
||||
for x in solves:
|
||||
json['solves'].append({'id':x.id, 'chal':x.chal.name, 'chalid':x.chalid,'team':x.teamid, 'value': x.chal.value, 'category':x.chal.category, 'time':unix_time(x.date)})
|
||||
return jsonify(json)
|
||||
|
||||
@app.route('/admin/statistics', methods=['GET'])
|
||||
@admins_only
|
||||
def admin_stats():
|
||||
db.session.commit()
|
||||
|
||||
teams_registered = db.session.query(db.func.count(Teams.id)).first()[0]
|
||||
site_hits = db.session.query(db.func.count(Tracking.id)).first()[0]
|
||||
wrong_count = db.session.query(db.func.count(WrongKeys.id)).first()[0]
|
||||
solve_count = db.session.query(db.func.count(Solves.id)).first()[0]
|
||||
challenge_count = db.session.query(db.func.count(Challenges.id)).first()[0]
|
||||
most_solved_chal = Solves.query.add_columns(db.func.count(Solves.chalid).label('solves')).group_by(Solves.chalid).order_by('solves DESC').first()
|
||||
least_solved_chal = Solves.query.add_columns(db.func.count(Solves.chalid).label('solves')).group_by(Solves.chalid).order_by('solves ASC').first()
|
||||
|
||||
db.session.close()
|
||||
|
||||
return render_template('admin/statistics.html', team_count=teams_registered,
|
||||
hit_count=site_hits,
|
||||
wrong_count=wrong_count,
|
||||
solve_count=solve_count,
|
||||
challenge_count=challenge_count,
|
||||
most_solved=most_solved_chal,
|
||||
least_solved = least_solved_chal
|
||||
)
|
||||
|
||||
|
||||
|
||||
@app.route('/admin/fails/<teamid>', methods=['GET'])
|
||||
@admins_only
|
||||
def admin_fails(teamid='all'):
|
||||
if teamid == "all":
|
||||
fails = WrongKeys.query.count()
|
||||
solves = Solves.query.count()
|
||||
db.session.close()
|
||||
json = {'fails':str(fails), 'solves': str(solves)}
|
||||
return jsonify(json)
|
||||
else:
|
||||
fails = WrongKeys.query.filter_by(team=teamid).count()
|
||||
solves = Solves.query.filter_by(teamid=teamid).count()
|
||||
db.session.close()
|
||||
json = {'fails':str(fails), 'solves': str(solves)}
|
||||
return jsonify(json)
|
||||
|
||||
|
||||
|
||||
@app.route('/admin/chal/new', methods=['POST'])
|
||||
def admin_create_chal():
|
||||
|
||||
files = request.files.getlist('files[]')
|
||||
|
||||
# Create challenge
|
||||
chal = Challenges(request.form['name'], request.form['desc'], request.form['value'], request.form['category'])
|
||||
db.session.add(chal)
|
||||
db.session.commit()
|
||||
|
||||
# Add keys
|
||||
key = Keys(chal.id, request.form['key'], request.form['key_type[0]'])
|
||||
db.session.add(key)
|
||||
db.session.commit()
|
||||
|
||||
for f in files:
|
||||
filename = secure_filename(f.filename)
|
||||
|
||||
if len(filename) <= 0:
|
||||
continue
|
||||
|
||||
md5hash = hashlib.md5(filename).hexdigest()
|
||||
|
||||
if not os.path.exists(os.path.join(app.config['UPLOAD_FOLDER'], md5hash)):
|
||||
os.makedirs(os.path.join(app.config['UPLOAD_FOLDER'], md5hash))
|
||||
|
||||
f.save(os.path.join(app.config['UPLOAD_FOLDER'], md5hash, filename))
|
||||
db_f = Files(chal.id, os.path.join(app.config['UPLOAD_FOLDER'], md5hash, filename))
|
||||
db.session.add(db_f)
|
||||
|
||||
db.session.commit()
|
||||
db.session.close()
|
||||
return redirect('/admin/chals')
|
||||
|
||||
@app.route('/admin/chal/update', methods=['POST'])
|
||||
def admin_update_chal():
|
||||
challenge=Challenges.query.filter_by(id=request.form['id']).first()
|
||||
challenge.name = request.form['name']
|
||||
challenge.description = request.form['desc']
|
||||
challenge.value = request.form['value']
|
||||
challenge.category = request.form['category']
|
||||
db.session.add(challenge)
|
||||
db.session.commit()
|
||||
db.session.close()
|
||||
return redirect('/admin/chals')
|
|
@ -0,0 +1,129 @@
|
|||
from CTFd import render_template, request, redirect, abort, jsonify, url_for, session
|
||||
from CTFd.utils import sha512, is_safe_url, authed, mailserver
|
||||
from CTFd.models import db, Teams
|
||||
|
||||
from itsdangerous import TimedSerializer, BadTimeSignature
|
||||
from passlib.hash import bcrypt_sha256
|
||||
from flask import current_app as app
|
||||
|
||||
import logging
|
||||
import time
|
||||
import re
|
||||
import os
|
||||
|
||||
def init_auth(app):
|
||||
@app.context_processor
|
||||
def inject_user():
|
||||
if authed():
|
||||
return dict(session)
|
||||
return dict()
|
||||
|
||||
@app.route('/reset_password', methods=['POST', 'GET'])
|
||||
@app.route('/reset_password/<data>', methods=['POST', 'GET'])
|
||||
def reset_password(data=None):
|
||||
if data is not None and request.method == "GET":
|
||||
return render_template('reset_password.html', mode='set')
|
||||
if data is not None and request.method == "POST":
|
||||
try:
|
||||
s = TimedSerializer(app.config['SECRET_KEY'])
|
||||
name = s.loads(data.decode('base64'), max_age=1800)
|
||||
except BadTimeSignature:
|
||||
return render_template('reset_password.html', errors=['Your link has expired'])
|
||||
team = Teams.query.filter_by(name=name).first()
|
||||
team.password = sha512(request.form['password'].strip())
|
||||
db.session.commit()
|
||||
db.session.close()
|
||||
return redirect('/login')
|
||||
|
||||
if request.method == 'POST':
|
||||
email = request.form['email'].strip()
|
||||
team = Teams.query.filter_by(email=email).first()
|
||||
if not team:
|
||||
return render_template('reset_password.html', errors=['Check your email'])
|
||||
s = TimedSerializer(app.config['SECRET_KEY'])
|
||||
token = s.dumps(team.name)
|
||||
text = """
|
||||
Did you initiate a password reset?
|
||||
|
||||
{0}/reset_password/{1}
|
||||
|
||||
""".format(app.config['HOST'], token.encode('base64'))
|
||||
|
||||
sendmail(email, text)
|
||||
|
||||
return render_template('reset_password.html', errors=['Check your email'])
|
||||
return render_template('reset_password.html')
|
||||
|
||||
@app.route('/register', methods=['POST', 'GET'])
|
||||
def register():
|
||||
if request.method == 'POST':
|
||||
errors = []
|
||||
name_len = len(request.form['name']) == 0
|
||||
names = Teams.query.add_columns('name', 'id').filter_by(name=request.form['name']).first()
|
||||
emails = Teams.query.add_columns('email', 'id').filter_by(email=request.form['email']).first()
|
||||
pass_len = len(request.form['password']) == 0
|
||||
valid_email = re.match("[^@]+@[^@]+\.[^@]+", request.form['email'])
|
||||
|
||||
if not valid_email:
|
||||
errors.append("That email doesn't look right")
|
||||
if names:
|
||||
errors.append('That team name is already taken')
|
||||
if emails:
|
||||
errors.append('That email has already been used')
|
||||
if pass_len:
|
||||
errors.append('Pick a longer password')
|
||||
if name_len:
|
||||
errors.append('Pick a longer team name')
|
||||
|
||||
if not errors:
|
||||
with app.app_context():
|
||||
team = Teams(request.form['name'], request.form['email'], request.form['password'])
|
||||
db.session.add(team)
|
||||
db.session.commit()
|
||||
if mailserver():
|
||||
sendmail(request.form['email'], "You've successfully registered for the CTF")
|
||||
|
||||
db.session.close()
|
||||
if len(errors) > 0:
|
||||
return render_template('register.html', errors=errors, name=request.form['name'], email=request.form['email'], password=request.form['password'])
|
||||
|
||||
logger = logging.getLogger('regs')
|
||||
logger.warn("[{0}] {1} registered with {2}".format(time.strftime("%m/%d/%Y %X"), request.form['name'], request.form['email']))
|
||||
return redirect('/login')
|
||||
else:
|
||||
return render_template('register.html')
|
||||
|
||||
@app.route('/login', methods=['POST', 'GET'])
|
||||
def login():
|
||||
if request.method == 'POST':
|
||||
errors = []
|
||||
# team = Teams.query.filter_by(name=request.form['name'], password=sha512(request.form['password'])).first()
|
||||
team = Teams.query.filter_by(name=request.form['name']).first()
|
||||
if team and bcrypt_sha256.verify(request.form['password'], team.password):
|
||||
# session.regenerate() # NO SESSION FIXATION FOR YOU
|
||||
session['username'] = team.name
|
||||
session['id'] = team.id
|
||||
session['admin'] = team.admin
|
||||
session['nonce'] = sha512(os.urandom(10))
|
||||
db.session.close()
|
||||
|
||||
logger = logging.getLogger('logins')
|
||||
logger.warn("[{0}] {1} logged in".format(time.strftime("%m/%d/%Y %X"), session['username']))
|
||||
|
||||
# if request.args.get('next') and is_safe_url(request.args.get('next')):
|
||||
# return redirect(request.args.get('next'))
|
||||
return redirect('/team/{0}'.format(team.id))
|
||||
else:
|
||||
errors.append("That account doesn't seem to exist")
|
||||
db.session.close()
|
||||
return render_template('login.html', errors=errors)
|
||||
else:
|
||||
db.session.close()
|
||||
return render_template('login.html')
|
||||
|
||||
|
||||
@app.route('/logout')
|
||||
def logout():
|
||||
if authed():
|
||||
session.clear()
|
||||
return redirect('/')
|
|
@ -0,0 +1,127 @@
|
|||
from flask import current_app as app, render_template, request, redirect, abort, jsonify, json as json_mod, url_for
|
||||
|
||||
from CTFd import session, logging
|
||||
from CTFd.utils import ctftime, authed, unix_time, get_kpm
|
||||
from CTFd.models import db, Challenges, Files, Solves, WrongKeys, Keys
|
||||
|
||||
import time
|
||||
|
||||
def init_challenges(app):
|
||||
@app.route('/challenges', methods=['GET'])
|
||||
def challenges():
|
||||
if not ctftime():
|
||||
return redirect('/')
|
||||
if authed():
|
||||
return render_template('chals.html')
|
||||
else:
|
||||
return redirect(url_for('login', next="challenges"))
|
||||
|
||||
@app.route('/chals', methods=['GET'])
|
||||
def chals():
|
||||
if not ctftime():
|
||||
return redirect('/')
|
||||
if authed():
|
||||
chals = Challenges.query.add_columns('id', 'name', 'value', 'description', 'category').order_by(Challenges.value).all()
|
||||
|
||||
json = {'game':[]}
|
||||
for x in chals:
|
||||
files = [ str(f.location) for f in Files.query.filter_by(chal=x.id).all() ]
|
||||
json['game'].append({'id':x[1], 'name':x[2], 'value':x[3], 'description':x[4], 'category':x[5], 'files':files})
|
||||
|
||||
db.session.close()
|
||||
return jsonify(json)
|
||||
else:
|
||||
db.session.close()
|
||||
return redirect('/login')
|
||||
|
||||
@app.route('/chals/solves')
|
||||
def chals_per_solves():
|
||||
if authed():
|
||||
solves = Solves.query.add_columns(db.func.count(Solves.chalid)).group_by(Solves.chalid).all()
|
||||
json = {}
|
||||
for chal, count in solves:
|
||||
json[chal.chal.name] = count
|
||||
return jsonify(json)
|
||||
return redirect(url_for('login', next="/chals/solves"))
|
||||
|
||||
@app.route('/solves')
|
||||
@app.route('/solves/<teamid>')
|
||||
def solves(teamid=None):
|
||||
if teamid is None:
|
||||
if authed():
|
||||
solves = Solves.query.filter_by(teamid=session['id']).all()
|
||||
else:
|
||||
return redirect('/login')
|
||||
else:
|
||||
solves = Solves.query.filter_by(teamid=teamid).all()
|
||||
db.session.close()
|
||||
json = {'solves':[]}
|
||||
for x in solves:
|
||||
json['solves'].append({'id':x.id, 'chal':x.chal.name, 'chalid':x.chalid,'team':x.teamid, 'value': x.chal.value, 'category':x.chal.category, 'time':unix_time(x.date)})
|
||||
return jsonify(json)
|
||||
|
||||
@app.route('/fails/<teamid>', methods=['GET'])
|
||||
def fails(teamid):
|
||||
fails = WrongKeys.query.filter_by(team=teamid).count()
|
||||
solves = Solves.query.filter_by(teamid=teamid).count()
|
||||
db.session.close()
|
||||
json = {'fails':str(fails), 'solves': str(solves)}
|
||||
return jsonify(json)
|
||||
|
||||
@app.route('/chal/<chalid>/solves', methods=['GET'])
|
||||
def who_solved(chalid):
|
||||
solves = Solves.query.filter_by(chalid=chalid)
|
||||
json = {'teams':[]}
|
||||
for solve in solves:
|
||||
json['teams'].append({'id':solve.team.id, 'name':solve.team.name, 'date':solve.date})
|
||||
return jsonify(json)
|
||||
|
||||
@app.route('/chal/<chalid>', methods=['POST'])
|
||||
def chal(chalid):
|
||||
if not ctftime():
|
||||
return redirect('/')
|
||||
if authed():
|
||||
logger = logging.getLogger('keys')
|
||||
data = (time.strftime("%m/%d/%Y %X"), session['username'], request.form['key'], get_kpm(session['id']))
|
||||
print "[{0}] {1} submitted {2} with kpm {3}".format(*data)
|
||||
if get_kpm(session['id']) > 10:
|
||||
wrong = WrongKeys(session['id'], chalid, request.form['key'])
|
||||
db.session.add(wrong)
|
||||
db.session.commit()
|
||||
db.session.close()
|
||||
logger.warn("[{0}] {1} submitted {2} with kpm {3} [TOO FAST]".format(*data))
|
||||
return "3" # Submitting too fast
|
||||
solves = Solves.query.filter_by(teamid=session['id'], chalid=chalid).first()
|
||||
if not solves:
|
||||
keys = Keys.query.filter_by(chal=chalid).all()
|
||||
key = request.form['key'].strip().lower()
|
||||
for x in keys:
|
||||
if x.key_type == 0: #static key
|
||||
if x.flag.strip().lower() == key:
|
||||
solve = Solves(chalid=chalid, teamid=session['id'], ip=request.remote_addr)
|
||||
db.session.add(solve)
|
||||
db.session.commit()
|
||||
db.session.close()
|
||||
logger.info("[{0}] {1} submitted {2} with kpm {3} [CORRECT]".format(*data))
|
||||
return "1" # key was correct
|
||||
elif x.key_type == 1: #regex
|
||||
res = re.match(str(x), key, re.IGNORECASE)
|
||||
if res and res.group() == key:
|
||||
solve = Solves(chalid=chalid, teamid=session['id'], ip=request.remote_addr)
|
||||
db.session.add(solve)
|
||||
db.session.commit()
|
||||
db.session.close()
|
||||
logger.info("[{0}] {1} submitted {2} with kpm {3} [CORRECT]".format(*data))
|
||||
return "1" # key was correct
|
||||
|
||||
wrong = WrongKeys(session['id'], chalid, request.form['key'])
|
||||
db.session.add(wrong)
|
||||
db.session.commit()
|
||||
db.session.close()
|
||||
logger.info("[{0}] {1} submitted {2} with kpm {3} [WRONG]".format(*data))
|
||||
return '0' # key was wrong
|
||||
else:
|
||||
logger.info("{0} submitted {1} with kpm {2} [ALREADY SOLVED]".format(*data))
|
||||
return "2" # challenge was already solved
|
||||
else:
|
||||
return "-1"
|
|
@ -0,0 +1,19 @@
|
|||
import os
|
||||
##### SERVER SETTINGS #####
|
||||
SECRET_KEY = os.urandom(64)
|
||||
SQLALCHEMY_DATABASE_URI = 'sqlite:///ctfd.db'
|
||||
SESSION_TYPE = "filesystem"
|
||||
SESSION_FILE_DIR = "/tmp/flask_session"
|
||||
SESSION_COOKIE_HTTPONLY = True
|
||||
HOST = ".ctfd.io"
|
||||
UPLOAD_FOLDER = 'static/uploads'
|
||||
|
||||
##### EMAIL #####
|
||||
CTF_NAME = ''
|
||||
MAIL_SERVER = ''
|
||||
MAIL_PORT = 0
|
||||
MAIL_USE_TLS = False
|
||||
MAIL_USE_SSL = False
|
||||
MAIL_USERNAME = ''
|
||||
MAIL_PASSWORD = ''
|
||||
ADMINS = []
|
|
@ -0,0 +1,14 @@
|
|||
from flask import current_app as app, render_template
|
||||
|
||||
def init_errors(app):
|
||||
@app.errorhandler(404)
|
||||
def page_not_found(error):
|
||||
return render_template('errors/404.html'), 404
|
||||
|
||||
@app.errorhandler(403)
|
||||
def forbidden(error):
|
||||
return render_template('errors/403.html'), 403
|
||||
|
||||
@app.errorhandler(500)
|
||||
def forbidden(error):
|
||||
return render_template('errors/500.html'), 500
|
|
@ -0,0 +1,169 @@
|
|||
#from CTFd import db
|
||||
from flask.ext.sqlalchemy import SQLAlchemy
|
||||
|
||||
from socket import inet_aton, inet_ntoa
|
||||
from struct import unpack, pack
|
||||
from passlib.hash import bcrypt_sha256
|
||||
|
||||
import datetime
|
||||
import hashlib
|
||||
|
||||
def sha512(string):
|
||||
return hashlib.sha512(string).hexdigest()
|
||||
|
||||
def ip2long(ip):
|
||||
return unpack('!I', inet_aton(ip))[0]
|
||||
|
||||
def long2ip(ip_int):
|
||||
return inet_ntoa(pack('!I', ip_int))
|
||||
|
||||
db = SQLAlchemy()
|
||||
|
||||
|
||||
class Pages(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
route = db.Column(db.String(80), unique=True)
|
||||
html = db.Column(db.Text)
|
||||
|
||||
def __init__(self, route, html):
|
||||
self.route = route
|
||||
self.html = html
|
||||
|
||||
def __repr__(self):
|
||||
return "<Tag {0} for challenge {1}>".format(self.tag, self.chal)
|
||||
|
||||
class Challenges(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(80))
|
||||
description = db.Column(db.Text)
|
||||
value = db.Column(db.Integer)
|
||||
category = db.Column(db.String(80))
|
||||
# add open category
|
||||
|
||||
def __init__(self, name, description, value, category):
|
||||
self.name = name
|
||||
self.description = description
|
||||
self.value = value
|
||||
self.category = category
|
||||
|
||||
def __repr__(self):
|
||||
return '<chal %r>' % self.name
|
||||
|
||||
class Tags(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
chal = db.Column(db.Integer, db.ForeignKey('challenges.id'))
|
||||
tag = db.Column(db.String(80))
|
||||
|
||||
def __init__(self, chal, tag):
|
||||
self.chal = chal
|
||||
self.tag = tag
|
||||
|
||||
def __repr__(self):
|
||||
return "<Tag {0} for challenge {1}>".format(self.tag, self.chal)
|
||||
|
||||
class Files(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
chal = db.Column(db.Integer, db.ForeignKey('challenges.id'))
|
||||
location = db.Column(db.Text)
|
||||
|
||||
def __init__(self, chal, location):
|
||||
self.chal = chal
|
||||
self.location = location
|
||||
|
||||
def __repr__(self):
|
||||
return "<File {0} for challenge {1}>".format(self.location, self.chal)
|
||||
|
||||
class Keys(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
chal = db.Column(db.Integer, db.ForeignKey('challenges.id'))
|
||||
key_type = db.Column(db.Integer)
|
||||
flag = db.Column(db.Text)
|
||||
|
||||
|
||||
def __init__(self, chal, flag, key_type):
|
||||
self.chal = chal
|
||||
self.flag = flag
|
||||
self.key_type = key_type
|
||||
|
||||
def __repr__(self):
|
||||
return self.flag
|
||||
|
||||
|
||||
class Teams(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(128), unique=True)
|
||||
email = db.Column(db.String(128), unique=True)
|
||||
password = db.Column(db.String(128))
|
||||
website = db.Column(db.String(128))
|
||||
affiliation = db.Column(db.String(128))
|
||||
country = db.Column(db.String(32))
|
||||
bracket = db.Column(db.String(32))
|
||||
banned = db.Column(db.Boolean)
|
||||
admin = db.Column(db.Boolean)
|
||||
|
||||
def __init__(self, name, email, password):
|
||||
self.name = name
|
||||
self.email = email
|
||||
self.password = bcrypt_sha256.encrypt(password)
|
||||
|
||||
def __repr__(self):
|
||||
return '<team %r>' % self.name
|
||||
|
||||
class Solves(db.Model):
|
||||
__table_args__ = (db.UniqueConstraint('chalid', 'teamid'), {})
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
chalid = db.Column(db.Integer, db.ForeignKey('challenges.id'))
|
||||
teamid = db.Column(db.Integer, db.ForeignKey('teams.id'))
|
||||
ip = db.Column(db.Integer)
|
||||
date = db.Column(db.DateTime, default=datetime.datetime.utcnow)
|
||||
team = db.relationship('Teams', foreign_keys="Solves.teamid", lazy='joined')
|
||||
chal = db.relationship('Challenges', foreign_keys="Solves.chalid", lazy='joined')
|
||||
# value = db.Column(db.Integer)
|
||||
|
||||
def __init__(self, chalid, teamid, ip):
|
||||
self.ip = ip2long(ip)
|
||||
self.chalid = chalid
|
||||
self.teamid = teamid
|
||||
# self.value = value
|
||||
|
||||
def __repr__(self):
|
||||
return '<solves %r>' % self.chal
|
||||
|
||||
|
||||
class WrongKeys(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
chal = db.Column(db.Integer, db.ForeignKey('challenges.id'))
|
||||
team = db.Column(db.Integer, db.ForeignKey('teams.id'))
|
||||
date = db.Column(db.DateTime, default=datetime.datetime.utcnow)
|
||||
flag = db.Column(db.Text)
|
||||
|
||||
def __init__(self, team, chal, flag):
|
||||
self.team = team
|
||||
self.chal = chal
|
||||
self.flag = flag
|
||||
|
||||
def __repr__(self):
|
||||
return '<wrong %r>' % self.flag
|
||||
|
||||
|
||||
class Tracking(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
ip = db.Column(db.BigInteger)
|
||||
team = db.Column(db.Integer, db.ForeignKey('teams.id'))
|
||||
date = db.Column(db.DateTime, default=datetime.datetime.utcnow)
|
||||
|
||||
def __init__(self, ip, team):
|
||||
self.ip = ip2long(ip)
|
||||
self.team = team
|
||||
|
||||
def __repr__(self):
|
||||
return '<ip %r>' % self.team
|
||||
|
||||
class Config(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
key = db.Column(db.Text)
|
||||
value = db.Column(db.Text)
|
||||
|
||||
def __init__(self, key, value):
|
||||
self.key = key
|
||||
self.value = value
|
|
@ -0,0 +1,50 @@
|
|||
from flask import current_app as app
|
||||
from CTFd import session, render_template, jsonify
|
||||
from CTFd.utils import unix_time
|
||||
from CTFd.models import db, Teams, Solves, Challenges
|
||||
|
||||
def init_scoreboard(app):
|
||||
@app.route('/scoreboard')
|
||||
def scoreboard():
|
||||
score = db.func.sum(Challenges.value).label('score')
|
||||
quickest = db.func.max(Solves.date).label('quickest')
|
||||
teams = db.session.query(Solves.teamid, Teams.name, score).join(Teams).join(Challenges).filter(Teams.banned == None).group_by(Solves.teamid).order_by(score.desc(), quickest)
|
||||
#teams = db.engine.execute("SELECT solves.teamid, teams.id, teams.name, SUM(value) as score, MAX(solves.date) as quickest FROM solves JOIN teams ON solves.teamid=teams.id INNER JOIN challenges ON solves.chalid=challenges.id WHERE teams.banned IS NULL GROUP BY solves.teamid ORDER BY score DESC, quickest ASC;")
|
||||
db.session.close()
|
||||
return render_template('scoreboard.html', teams=teams)
|
||||
|
||||
@app.route('/scores')
|
||||
def scores():
|
||||
#teams = db.engine.execute("SELECT solves.teamid, teams.id, teams.name, SUM(value) as score, MAX(solves.date) as quickest FROM solves JOIN teams ON solves.teamid=teams.id INNER JOIN challenges ON solves.chalid=challenges.id WHERE teams.banned IS NULL GROUP BY solves.teamid ORDER BY score DESC, quickest ASC;")
|
||||
score = db.func.sum(Challenges.value).label('score')
|
||||
quickest = db.func.max(Solves.date).label('quickest')
|
||||
teams = db.session.query(Solves.teamid, Teams.name, score).join(Teams).join(Challenges).filter(Teams.banned == None).group_by(Solves.teamid).order_by(score.desc(), quickest)
|
||||
db.session.close()
|
||||
json = {'teams':[]}
|
||||
for i, x in enumerate(teams):
|
||||
json['teams'].append({'place':i+1, 'id':x.teamid, 'name':x.name,'score':int(x.score)})
|
||||
return jsonify(json)
|
||||
|
||||
@app.route('/top/<count>')
|
||||
def topteams(count):
|
||||
try:
|
||||
count = int(count)
|
||||
except:
|
||||
count = 10
|
||||
if count > 20 or count < 0:
|
||||
count = 10
|
||||
|
||||
json = {'scores':{}}
|
||||
|
||||
#teams = db.engine.execute("SELECT solves.teamid, teams.id, teams.name, SUM(value) as score, MAX(solves.date) as quickest FROM solves JOIN teams ON solves.teamid=teams.id INNER JOIN challenges ON solves.chalid=challenges.id WHERE teams.banned IS NULL GROUP BY solves.teamid ORDER BY score DESC, quickest ASC LIMIT {0};".format(count))
|
||||
score = db.func.sum(Challenges.value).label('score')
|
||||
teams = db.session.query(Solves.teamid, Teams.name, score, db.func.max(Solves.date).label('quickest')).join(Teams).join(Challenges).filter(Teams.banned == None).group_by(Solves.teamid).order_by(score.desc(), Solves.date).limit(count)
|
||||
|
||||
|
||||
for team in teams:
|
||||
solves = Solves.query.filter_by(teamid=team.teamid).all()
|
||||
json['scores'][team.name] = []
|
||||
for x in solves:
|
||||
json['scores'][team.name].append({'id':x.teamid, 'chal':x.chalid, 'team':x.teamid, 'value': x.chal.value, 'time':unix_time(x.date)})
|
||||
|
||||
return jsonify(json)
|
|
@ -0,0 +1,124 @@
|
|||
from CTFd import session
|
||||
from CTFd.models import db, WrongKeys, Pages, Config
|
||||
|
||||
from urlparse import urlparse, urljoin
|
||||
from functools import wraps
|
||||
from flask import current_app as app, g, request, redirect, url_for
|
||||
from socket import inet_aton, inet_ntoa
|
||||
from struct import unpack, pack
|
||||
|
||||
import time
|
||||
import datetime
|
||||
import hashlib
|
||||
import json
|
||||
|
||||
def init_utils(app):
|
||||
app.jinja_env.filters['unix_time'] = unix_time
|
||||
app.jinja_env.filters['unix_time_millis'] = unix_time_millis
|
||||
app.jinja_env.filters['long2ip'] = long2ip
|
||||
app.jinja_env.globals.update(pages=pages)
|
||||
|
||||
def pages():
|
||||
pages = Pages.query.filter(Pages.route!="index").all()
|
||||
return pages
|
||||
|
||||
def authed():
|
||||
try:
|
||||
if session['id']:
|
||||
return True
|
||||
except KeyError:
|
||||
pass
|
||||
return False
|
||||
|
||||
def is_setup():
|
||||
setup = Config.query.filter_by(key='setup').first()
|
||||
if setup:
|
||||
return setup.value
|
||||
else:
|
||||
return False
|
||||
|
||||
def is_admin():
|
||||
if authed():
|
||||
return session['admin']
|
||||
else:
|
||||
return False
|
||||
|
||||
def admins_only(f):
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
if session.get('admin', None) is None:
|
||||
return redirect(url_for('login', next=request.url))
|
||||
return f(*args, **kwargs)
|
||||
return decorated_function
|
||||
|
||||
|
||||
def ctftime():
|
||||
""" Checks whether it's CTF time or not. """
|
||||
|
||||
start = Config.query.filter_by(key="start").first().value
|
||||
end = Config.query.filter_by(key="end").first().value
|
||||
|
||||
if start:
|
||||
start = int(start)
|
||||
if end:
|
||||
end = int(end)
|
||||
|
||||
if start and end:
|
||||
if start < time.time() and time.time() < end:
|
||||
# Within the two time bounds
|
||||
return True
|
||||
|
||||
if start < time.time() and end is None:
|
||||
# CTF starts on a date but never ends
|
||||
return True
|
||||
|
||||
if start is None and time.time() < end:
|
||||
# CTF started but ends at a date
|
||||
return True
|
||||
|
||||
if start is None and end is None:
|
||||
# CTF has no time requirements
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def unix_time(dt):
|
||||
epoch = datetime.datetime.utcfromtimestamp(0)
|
||||
delta = dt - epoch
|
||||
return int(delta.total_seconds())
|
||||
|
||||
def unix_time_millis(dt):
|
||||
return unix_time(dt) * 1000
|
||||
|
||||
def long2ip(ip_int):
|
||||
return inet_ntoa(pack('!I', ip_int))
|
||||
|
||||
def ip2long(ip):
|
||||
return unpack('!I', inet_aton(ip))[0]
|
||||
|
||||
def get_kpm(teamid): # keys per minute
|
||||
one_min_ago = datetime.datetime.utcnow() + datetime.timedelta(minutes=-1)
|
||||
return len(db.session.query(WrongKeys).filter(WrongKeys.team == teamid, WrongKeys.date >= one_min_ago).all())
|
||||
|
||||
|
||||
def mailserver():
|
||||
if app.config['MAIL_SERVER'] and app.config['MAIL_PORT'] and app.config['ADMINS']:
|
||||
return True
|
||||
return False
|
||||
|
||||
def sendmail(addr, text):
|
||||
try:
|
||||
msg = Message("Message from {0}".format(app.config['CTF_NAME']), sender = app.config['ADMINS'][0], recipients = [addr])
|
||||
msg.body = text
|
||||
mail.send(msg)
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
|
||||
def is_safe_url(target):
|
||||
ref_url = urlparse(request.host_url)
|
||||
test_url = urlparse(urljoin(request.host_url, target))
|
||||
return test_url.scheme in ('http', 'https') and ref_url.netloc == test_url.netloc
|
||||
|
||||
def sha512(string):
|
||||
return hashlib.sha512(string).hexdigest()
|
|
@ -0,0 +1,167 @@
|
|||
from flask import current_app as app, render_template, render_template_string, request, redirect, abort, jsonify, json as json_mod, url_for
|
||||
from CTFd import session
|
||||
from CTFd.utils import authed, ip2long, long2ip, is_setup
|
||||
from CTFd.models import db, Teams, Solves, Challenges, WrongKeys, Keys, Tags, Files, Tracking, Pages, Config
|
||||
|
||||
from jinja2.exceptions import TemplateNotFound
|
||||
|
||||
from collections import OrderedDict
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import json
|
||||
import os
|
||||
|
||||
def init_views(app):
|
||||
@app.before_request
|
||||
def tracker():
|
||||
if authed():
|
||||
if not Tracking.query.filter_by(ip=ip2long(request.remote_addr)).first():
|
||||
visit = Tracking(request.remote_addr, session['id'])
|
||||
db.session.add(visit)
|
||||
db.session.commit()
|
||||
db.session.close()
|
||||
|
||||
@app.before_request
|
||||
def csrf():
|
||||
if authed() and request.method == "POST":
|
||||
if session['nonce'] != request.form.get('nonce'):
|
||||
abort(403)
|
||||
|
||||
@app.before_request
|
||||
def redirect_setup():
|
||||
if request.path == "/static/css/style.css":
|
||||
return
|
||||
if not is_setup() and request.path != "/setup":
|
||||
return redirect('/setup')
|
||||
|
||||
@app.before_first_request
|
||||
def needs_setup():
|
||||
if not is_setup():
|
||||
return redirect('/setup')
|
||||
|
||||
@app.route('/setup', methods=['GET', 'POST'])
|
||||
def setup():
|
||||
# with app.app_context():
|
||||
# admin = Teams.query.filter_by(admin=True).first()
|
||||
|
||||
if not is_setup():
|
||||
if request.method == 'POST':
|
||||
## Admin user
|
||||
name = request.form['name']
|
||||
email = request.form['email']
|
||||
password = request.form['password']
|
||||
admin = Teams(name, email, password)
|
||||
admin.admin = True
|
||||
|
||||
## Index page
|
||||
html = request.form['html']
|
||||
page = Pages('index', html)
|
||||
|
||||
## Start time
|
||||
start = Config('start', None)
|
||||
end = Config('end', None)
|
||||
|
||||
setup = Config('setup', True)
|
||||
|
||||
db.session.add(admin)
|
||||
db.session.add(page)
|
||||
db.session.add(start)
|
||||
db.session.add(end)
|
||||
db.session.add(setup)
|
||||
db.session.commit()
|
||||
app.setup = False
|
||||
return redirect('/')
|
||||
return render_template('setup.html')
|
||||
return redirect('/')
|
||||
|
||||
# Static HTML files
|
||||
@app.route("/", defaults={'template': 'index'})
|
||||
@app.route("/<template>")
|
||||
def static_html(template):
|
||||
try:
|
||||
return render_template('%s.html' % template)
|
||||
except TemplateNotFound:
|
||||
page = Pages.query.filter_by(route=template).first()
|
||||
if page:
|
||||
return render_template_string('{% extends "base.html" %}{% block content %}' + page.html + '{% endblock %}')
|
||||
else:
|
||||
abort(404)
|
||||
|
||||
@app.route('/teams')
|
||||
def teams():
|
||||
teams = Teams.query.all()
|
||||
return render_template('teams.html', teams=teams)
|
||||
|
||||
@app.route('/team/<teamid>', methods=['GET', 'POST'])
|
||||
def team(teamid):
|
||||
user = Teams.query.filter_by(id=teamid).first()
|
||||
solves = Solves.query.filter_by(teamid=teamid).all()
|
||||
db.session.close()
|
||||
|
||||
if request.method == 'GET':
|
||||
return render_template('team.html', solves=solves, team=user)
|
||||
elif request.method == 'POST':
|
||||
json = {'solves':[]}
|
||||
for x in solves:
|
||||
json['solves'].append({'id':x.id, 'chal':x.chalid, 'team':x.teamid})
|
||||
return jsonify(json)
|
||||
|
||||
|
||||
@app.route('/profile', methods=['POST', 'GET'])
|
||||
def profile():
|
||||
if authed():
|
||||
if request.method == "POST":
|
||||
errors = []
|
||||
|
||||
name = request.form.get('name')
|
||||
email = request.form.get('email')
|
||||
|
||||
if 'password' in request.form.keys() and not len(request.form['password']) == 0:
|
||||
password = request.form.get('password')
|
||||
|
||||
website = request.form.get('website')
|
||||
affiliation = request.form.get('affiliation')
|
||||
country = request.form.get('country')
|
||||
|
||||
names = Teams.query.filter_by(name=name).first()
|
||||
emails = Teams.query.filter_by(email=email).first()
|
||||
valid_email = re.match("[^@]+@[^@]+\.[^@]+", email)
|
||||
|
||||
name_len = len(request.form['name']) == 0
|
||||
|
||||
if not bcrypt_sha256.verify(request.form.get('confirm').strip(), names.password):
|
||||
errors.append("Your old password doesn't match what we have.")
|
||||
if not valid_email:
|
||||
errors.append("That email doesn't look right")
|
||||
if names and name!=session['username']:
|
||||
errors.append('That team name is already taken')
|
||||
if emails and emails.id != session['id']:
|
||||
errors.append('That email has already been used')
|
||||
if name_len:
|
||||
errors.append('Pick a longer team name')
|
||||
|
||||
if len(errors) > 0:
|
||||
return render_template('profile.html', name=name, email=email, website=website, affiliation=affiliation, country=country, errors=errors)
|
||||
else:
|
||||
team = Teams.query.filter_by(id=session['id']).first()
|
||||
team.name = name
|
||||
team.email = email
|
||||
team.password = bcrypt_sha256.encrypt(password)
|
||||
team.website = website
|
||||
team.affiliation = affiliation
|
||||
team.country = country
|
||||
db.session.commit()
|
||||
db.session.close()
|
||||
return redirect('/profile')
|
||||
else:
|
||||
user = Teams.query.filter_by(id=session['id']).first()
|
||||
name = user.name
|
||||
email = user.email
|
||||
website = user.website
|
||||
affiliation = user.affiliation
|
||||
country = user.country
|
||||
return render_template('profile.html', name=name, email=email, website=website, affiliation=affiliation, country=country)
|
||||
else:
|
||||
return redirect('/login')
|
10
README.md
10
README.md
|
@ -1,4 +1,10 @@
|
|||
CTFd
|
||||
![](https://github.com/isislab/CTFd/blob/master/static/img/logo.png)
|
||||
====
|
||||
|
||||
CTFs as you need them
|
||||
CTFd is a CTF in a can. Easily modifiable and has everything you need to run a jeopardy style CTF.
|
||||
|
||||
Install:
|
||||
1. `./prepare.sh` to install dependencies using apt.
|
||||
2. Modify [CTFd/config.py](https://github.com/isislab/CTFd/blob/master/CTFd/config.py) to your liking.
|
||||
3. Use `python serve.py` in a terminal to drop into debug mode.
|
||||
4. [Here](http://flask.pocoo.org/docs/0.10/deploying/) are some Flask deployment options
|
||||
|
|
|
@ -0,0 +1,942 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from CTFd.models import Teams, Solves, Challenges, WrongKeys, Keys, Tags, Files, Tracking
|
||||
from CTFd import create_app
|
||||
from random import randint
|
||||
|
||||
import datetime
|
||||
import random
|
||||
import hashlib
|
||||
import os
|
||||
import sys
|
||||
|
||||
app = create_app(sys.argv[1], sys.argv[2], sys.argv[3])
|
||||
|
||||
USER_AMOUNT = 50
|
||||
CHAL_AMOUNT = 20
|
||||
|
||||
categories = [
|
||||
'Exploitation',
|
||||
'Reversing',
|
||||
'Web',
|
||||
'Forensics',
|
||||
'Scripting',
|
||||
'Cryptography',
|
||||
'Networking',
|
||||
]
|
||||
lorems = [
|
||||
'Lorem',
|
||||
'ipsum',
|
||||
'dolor',
|
||||
'sit',
|
||||
'amet,',
|
||||
'consectetur',
|
||||
'adipiscing',
|
||||
'elit.',
|
||||
'Proin',
|
||||
'fringilla',
|
||||
'elit',
|
||||
'velit,',
|
||||
'sed',
|
||||
'scelerisque',
|
||||
'tellus',
|
||||
'dapibus',
|
||||
'vel.',
|
||||
'Aenean',
|
||||
'at',
|
||||
'urna',
|
||||
'porta,',
|
||||
'fringilla',
|
||||
'erat',
|
||||
'eget,',
|
||||
'lobortis',
|
||||
'quam.',
|
||||
'Praesent',
|
||||
'luctus,',
|
||||
'quam',
|
||||
'at',
|
||||
'consequat',
|
||||
'luctus,',
|
||||
'mauris',
|
||||
'sem',
|
||||
'pretium',
|
||||
'metus,',
|
||||
'eu',
|
||||
'viverra',
|
||||
'dui',
|
||||
'leo',
|
||||
'in',
|
||||
'tortor.',
|
||||
'Cras',
|
||||
'iaculis',
|
||||
'enim',
|
||||
'erat,',
|
||||
'sed',
|
||||
'gravida',
|
||||
'velit',
|
||||
'consectetur',
|
||||
'a.',
|
||||
'Duis',
|
||||
'eget',
|
||||
'fermentum',
|
||||
'elit.',
|
||||
'Vivamus',
|
||||
'laoreet',
|
||||
'elementum',
|
||||
'massa,',
|
||||
'ut',
|
||||
'sodales',
|
||||
'mi',
|
||||
'gravida',
|
||||
'at.',
|
||||
'Vivamus',
|
||||
'dignissim',
|
||||
'in',
|
||||
'eros',
|
||||
'non',
|
||||
'iaculis.',
|
||||
'Vivamus',
|
||||
'nec',
|
||||
'sem',
|
||||
'fringilla,',
|
||||
'semper',
|
||||
'lectus',
|
||||
'in,',
|
||||
'malesuada',
|
||||
'tellus.',
|
||||
'Vestibulum',
|
||||
'mattis',
|
||||
'commodo',
|
||||
'enim',
|
||||
'sit',
|
||||
'amet',
|
||||
'scelerisque.',
|
||||
'Proin',
|
||||
'at',
|
||||
'condimentum',
|
||||
'nisi,',
|
||||
'nec',
|
||||
'fringilla',
|
||||
'ante.',
|
||||
'Vestibulum',
|
||||
'sit',
|
||||
'amet',
|
||||
'neque',
|
||||
'sit',
|
||||
'amet',
|
||||
'elit',
|
||||
'placerat',
|
||||
'interdum',
|
||||
'egestas',
|
||||
'ac',
|
||||
'malesuada',
|
||||
'quis',
|
||||
'arcu',
|
||||
'ac',
|
||||
'blandit.',
|
||||
'Vivamus',
|
||||
'in',
|
||||
'massa',
|
||||
'a',
|
||||
'purus',
|
||||
'bibendum',
|
||||
'sagittis.',
|
||||
'Nunc',
|
||||
'venenatis',
|
||||
'lacus',
|
||||
'sed',
|
||||
'nulla',
|
||||
'dapibus,',
|
||||
'consequat',
|
||||
'laoreet',
|
||||
'nisi',
|
||||
'faucibus.',
|
||||
'Nam',
|
||||
'consequat',
|
||||
'viverra',
|
||||
'nibh',
|
||||
'a',
|
||||
'cursus.',
|
||||
'Phasellus',
|
||||
'tristique',
|
||||
'justo',
|
||||
'vitae',
|
||||
'rutrum',
|
||||
'pharetra.',
|
||||
'Sed',
|
||||
'sed',
|
||||
'porttitor',
|
||||
'lacus.',
|
||||
'Nam',
|
||||
'ornare',
|
||||
'sit',
|
||||
'amet',
|
||||
'nisi',
|
||||
'imperdiet',
|
||||
'vulputate.',
|
||||
'Maecenas',
|
||||
'hendrerit',
|
||||
'ullamcorper',
|
||||
'elit,',
|
||||
'sed',
|
||||
'pellentesque',
|
||||
'lacus',
|
||||
'bibendum',
|
||||
'sit',
|
||||
'amet.',
|
||||
'Aliquam',
|
||||
'consectetur',
|
||||
'odio',
|
||||
'quis',
|
||||
'tellus',
|
||||
'ornare,',
|
||||
'id',
|
||||
'malesuada',
|
||||
'dui',
|
||||
'rhoncus.',
|
||||
'Quisque',
|
||||
'fringilla',
|
||||
'pellentesque',
|
||||
'nulla',
|
||||
'id',
|
||||
'congue.',
|
||||
'Nulla',
|
||||
'ultricies',
|
||||
'dolor',
|
||||
'tristique',
|
||||
'facilisis',
|
||||
'at',
|
||||
'accumsan',
|
||||
'nisi.',
|
||||
'Praesent',
|
||||
'commodo,',
|
||||
'mauris',
|
||||
'sit',
|
||||
'amet',
|
||||
'placerat',
|
||||
'condimentum,',
|
||||
'nibh',
|
||||
'leo',
|
||||
'pulvinar',
|
||||
'justo,',
|
||||
'vel',
|
||||
'dignissim',
|
||||
'mi',
|
||||
'dolor',
|
||||
'et',
|
||||
'est.',
|
||||
'Nulla',
|
||||
'facilisi.',
|
||||
'Sed',
|
||||
'nunc',
|
||||
'est,',
|
||||
'lobortis',
|
||||
'id',
|
||||
'diam',
|
||||
'nec,',
|
||||
'vulputate',
|
||||
'varius',
|
||||
'orci.',
|
||||
'Maecenas',
|
||||
'iaculis',
|
||||
'vehicula',
|
||||
'eros',
|
||||
'eu',
|
||||
'congue.',
|
||||
'Nam',
|
||||
'tempor',
|
||||
'commodo',
|
||||
'lobortis.',
|
||||
'Donec',
|
||||
'eget',
|
||||
'posuere',
|
||||
'dolor,',
|
||||
'ut',
|
||||
'rhoncus',
|
||||
'tortor.',
|
||||
'Donec',
|
||||
'et',
|
||||
'quam',
|
||||
'quis',
|
||||
'urna',
|
||||
'rhoncus',
|
||||
'fermentum',
|
||||
'et',
|
||||
'ut',
|
||||
'tellus.',
|
||||
'Aliquam',
|
||||
'erat',
|
||||
'volutpat.',
|
||||
'Morbi',
|
||||
'porttitor',
|
||||
'ante',
|
||||
'nec',
|
||||
'porta',
|
||||
'mollis.',
|
||||
'Ut',
|
||||
'sodales',
|
||||
'pellentesque',
|
||||
'rutrum.',
|
||||
'Nullam',
|
||||
'elit',
|
||||
'eros,',
|
||||
'sollicitudin',
|
||||
'ac',
|
||||
'rutrum',
|
||||
'sit',
|
||||
'amet,',
|
||||
'eleifend',
|
||||
'vel',
|
||||
'nulla.',
|
||||
'Morbi',
|
||||
'quis',
|
||||
'lacinia',
|
||||
'nisi.',
|
||||
'Integer',
|
||||
'at',
|
||||
'neque',
|
||||
'vel',
|
||||
'velit',
|
||||
'tincidunt',
|
||||
'elementum',
|
||||
'lobortis',
|
||||
'sit',
|
||||
'amet',
|
||||
'tellus.',
|
||||
'Nunc',
|
||||
'volutpat',
|
||||
'diam',
|
||||
'ac',
|
||||
'diam',
|
||||
'lacinia,',
|
||||
'id',
|
||||
'molestie',
|
||||
'quam',
|
||||
'eu',
|
||||
'ultricies',
|
||||
'ligula.',
|
||||
'Duis',
|
||||
'iaculis',
|
||||
'massa',
|
||||
'massa,',
|
||||
'eget',
|
||||
'venenatis',
|
||||
'dolor',
|
||||
'fermentum',
|
||||
'laoreet.',
|
||||
'Nam',
|
||||
'posuere,',
|
||||
'erat',
|
||||
'quis',
|
||||
'tempor',
|
||||
'consequat,',
|
||||
'purus',
|
||||
'erat',
|
||||
'hendrerit',
|
||||
'arcu,',
|
||||
'nec',
|
||||
'aliquam',
|
||||
'ligula',
|
||||
'augue',
|
||||
'vitae',
|
||||
'felis.',
|
||||
'Vestibulum',
|
||||
'tincidunt',
|
||||
'ipsum',
|
||||
'vel',
|
||||
'pharetra',
|
||||
'lacinia.',
|
||||
'Quisque',
|
||||
'dignissim,',
|
||||
'arcu',
|
||||
'non',
|
||||
'feugiat',
|
||||
'semper,',
|
||||
'felis',
|
||||
'est',
|
||||
'commodo',
|
||||
'lorem,',
|
||||
'malesuada',
|
||||
'elementum',
|
||||
'nibh',
|
||||
'lectus',
|
||||
'porttitor',
|
||||
'nisi.',
|
||||
'Duis',
|
||||
'non',
|
||||
'lacinia',
|
||||
'nisl.',
|
||||
'Etiam',
|
||||
'ante',
|
||||
'nisl,',
|
||||
'mattis',
|
||||
'eget',
|
||||
'convallis',
|
||||
'vel,',
|
||||
'ullamcorper',
|
||||
'ac',
|
||||
'nisl.',
|
||||
'Duis',
|
||||
'eu',
|
||||
'massa',
|
||||
'at',
|
||||
'urna',
|
||||
'laoreet',
|
||||
'convallis.',
|
||||
'Donec',
|
||||
'tincidunt',
|
||||
'sapien',
|
||||
'sit',
|
||||
'amet',
|
||||
'varius',
|
||||
'eu',
|
||||
'dignissim',
|
||||
'tortor,',
|
||||
'elementum',
|
||||
'gravida',
|
||||
'eros.',
|
||||
'Cras',
|
||||
'viverra',
|
||||
'accumsan',
|
||||
'erat,',
|
||||
'et',
|
||||
'euismod',
|
||||
'dui',
|
||||
'placerat',
|
||||
'ac.',
|
||||
'Ut',
|
||||
'tortor',
|
||||
'arcu,',
|
||||
'euismod',
|
||||
'vitae',
|
||||
'aliquam',
|
||||
'in,',
|
||||
'interdum',
|
||||
'vitae',
|
||||
'magna.',
|
||||
'Vestibulum',
|
||||
'leo',
|
||||
'ante,',
|
||||
'posuere',
|
||||
'eget',
|
||||
'est',
|
||||
'non,',
|
||||
'adipiscing',
|
||||
'ultrices',
|
||||
'erat.',
|
||||
'Donec',
|
||||
'suscipit',
|
||||
'felis',
|
||||
'molestie,',
|
||||
'ultricies',
|
||||
'dui',
|
||||
'a,',
|
||||
'facilisis',
|
||||
'magna.',
|
||||
'Cum',
|
||||
'sociis',
|
||||
'natoque',
|
||||
'penatibus',
|
||||
'et',
|
||||
'magnis',
|
||||
'dis',
|
||||
'parturient',
|
||||
'montes,',
|
||||
'nascetur',
|
||||
'ridiculus',
|
||||
'mus.',
|
||||
'Nulla',
|
||||
'quis',
|
||||
'odio',
|
||||
'sit',
|
||||
'amet',
|
||||
'ante',
|
||||
'tristique',
|
||||
'accumsan',
|
||||
'ut',
|
||||
'iaculis',
|
||||
'neque.',
|
||||
'Vivamus',
|
||||
'in',
|
||||
'venenatis',
|
||||
'enim.',
|
||||
'Nunc',
|
||||
'dignissim',
|
||||
'justo',
|
||||
'neque,',
|
||||
'sed',
|
||||
'ultricies',
|
||||
'justo',
|
||||
'dictum',
|
||||
'in.',
|
||||
'Nulla',
|
||||
'eget',
|
||||
'nunc',
|
||||
'ac',
|
||||
'arcu',
|
||||
'vestibulum',
|
||||
'bibendum',
|
||||
'vitae',
|
||||
'quis',
|
||||
'tellus.',
|
||||
'Morbi',
|
||||
'bibendum,',
|
||||
'quam',
|
||||
'ac',
|
||||
'cursus',
|
||||
'posuere,',
|
||||
'purus',
|
||||
'lectus',
|
||||
'tempor',
|
||||
'est,',
|
||||
'eu',
|
||||
'iaculis',
|
||||
'quam',
|
||||
'enim',
|
||||
'a',
|
||||
'nibh.',
|
||||
'Etiam',
|
||||
'consequat',
|
||||
'libero',
|
||||
'vitae',
|
||||
'ullamcorper',
|
||||
'tincidunt.',
|
||||
]
|
||||
hipsters = ['Ethnic', 'narwhal', 'pickled', 'Odd', 'Future', 'cliche', 'VHS', 'whatever', 'Etsy', 'American', 'Apparel', 'kitsch', 'wolf', 'mlkshk', 'fashion', 'axe', 'ethnic', 'banh', 'mi', 'cornhole', 'scenester', 'Echo', 'Park', 'Dreamcatcher', 'tofu', 'fap', 'selvage', 'authentic', 'cliche', 'High', 'Life', 'brunch', 'pork', 'belly', 'viral', 'XOXO', 'drinking', 'vinegar', 'bitters', 'Wayfarers', 'gastropub', 'dreamcatcher', 'chillwave', 'Shoreditch', 'kale', 'chips', 'swag', 'street', 'art', 'put', 'a', 'bird', 'on', 'it', 'Vice', 'synth', 'cliche', 'retro', 'Master', 'cleanse', 'ugh', 'Austin', 'slow-carb', 'small', 'batch', 'Hashtag', 'food', 'truck', 'deep', 'v', 'semiotics', 'chia', 'normcore', 'bicycle', 'rights', 'Austin', 'drinking', 'vinegar', 'hella', 'readymade', 'farm-to-table', 'Wes', 'Anderson', 'put', 'a', 'bird', 'on', 'it', 'freegan', 'Synth', 'lo-fi', 'food', 'truck', 'chambray', 'Shoreditch', 'cliche', 'kogiSynth', 'lo-fi', 'fap', 'single-origin', 'coffee', 'brunch', 'butcher', 'Pickled', 'Etsy', 'locavore', 'forage', 'pug', 'stumptown', 'occupy', 'PBR&B', 'actually', 'shabby', 'chic', 'church-key', 'disrupt', 'lomo', 'hoodie', 'Tumblr', 'biodiesel', 'Pinterest', 'butcher', 'Hella', 'Carles', 'pour-over', 'YOLO', 'VHS', 'literally', 'Selvage', 'narwhal', 'flexitarian', 'wayfarers', 'kitsch', 'bespoke', 'sriracha', 'Banh', 'mi', '8-bit', 'cornhole', 'viral', 'Tonx', 'keytar', 'gastropub', 'YOLO', 'hashtag', 'food', 'truck', '3', 'wolf', 'moonFingerstache', 'flexitarian', 'craft', 'beer', 'shabby', 'chic', '8-bit', 'try-hard', 'semiotics', 'Helvetica', 'keytar', 'PBR', 'four', 'loko', 'scenester', 'keytar', '3', 'wolf', 'moon', 'sriracha', 'gluten-free', 'literally', 'try-hard', 'put', 'a', 'bird', 'on', 'it', 'cornhole', 'blog', 'fanny', 'pack', 'Mumblecore', 'pickled', 'distillery', 'butcher', 'Ennui', 'tote', 'bag', 'letterpress', 'disrupt', 'keffiyeh', 'art', 'party', 'aesthetic', 'Helvetica', 'stumptown', 'Wes', 'Anderson', 'next', 'level', "McSweeney's", 'cornhole', 'Schlitz', 'skateboard', 'pop-up', 'Chillwave', 'biodiesel', 'semiotics', 'seitan', 'authentic', 'bicycle', 'rights', 'wolf', 'pork', 'belly', 'letterpress', 'locavore', 'whatever', 'fixie', 'viral', 'mustache', 'beard', 'Hashtag', 'sustainable', 'lomo', 'cardigan', 'lo-fiWilliamsburg', 'craft', 'beer', 'bitters', 'iPhone', 'gastropub', 'messenger', 'bag', 'Organic', 'post-ironic', 'fingerstache', 'ennui', 'banh', 'mi', 'Art', 'party', 'bitters', 'twee', 'bespoke', 'church-key', 'Intelligentsia', 'sriracha', 'Echo', 'Park', 'Tofu', 'locavore', 'street', 'art', 'freegan', 'farm-to-table', 'distillery', 'hoodie', 'swag', 'ugh', 'YOLO', 'VHS', 'Cred', 'hella', 'readymade', 'distillery', 'Banh', 'mi', 'Echo', 'Park', "McSweeney's,", 'mlkshk', 'photo', 'booth', 'swag', 'Odd', 'Future', 'squid', 'Tonx', 'craft', 'beer', 'High', 'Life', 'tousled', 'PBR', 'you', 'probably', "haven't", 'heard', 'of', 'them', 'locavore', 'PBR&B', 'street', 'art', 'pop-up', 'raw', 'denim']
|
||||
names = [
|
||||
'James',
|
||||
'John',
|
||||
'Robert',
|
||||
'Michael',
|
||||
'William',
|
||||
'David',
|
||||
'Richard',
|
||||
'Joseph',
|
||||
'Charles',
|
||||
'Thomas',
|
||||
'Christopher',
|
||||
'Daniel',
|
||||
'Matthew',
|
||||
'Donald',
|
||||
'Anthony',
|
||||
'Paul',
|
||||
'Mark',
|
||||
'George',
|
||||
'Steven',
|
||||
'Kenneth',
|
||||
'Andrew',
|
||||
'Edward',
|
||||
'Brian',
|
||||
'Joshua',
|
||||
'Kevin',
|
||||
'Ronald',
|
||||
'Timothy',
|
||||
'Jason',
|
||||
'Jeffrey',
|
||||
'Gary',
|
||||
'Ryan',
|
||||
'Nicholas',
|
||||
'Eric',
|
||||
'Stephen',
|
||||
'Jacob',
|
||||
'Larry',
|
||||
'Frank',
|
||||
'Jonathan',
|
||||
'Scott',
|
||||
'Justin',
|
||||
'Raymond',
|
||||
'Brandon',
|
||||
'Gregory',
|
||||
'Samuel',
|
||||
'Patrick',
|
||||
'Benjamin',
|
||||
'Jack',
|
||||
'Dennis',
|
||||
'Jerry',
|
||||
'Alexander',
|
||||
'Tyler',
|
||||
'Douglas',
|
||||
'Henry',
|
||||
'Peter',
|
||||
'Walter',
|
||||
'Aaron',
|
||||
'Jose',
|
||||
'Adam',
|
||||
'Harold',
|
||||
'Zachary',
|
||||
'Nathan',
|
||||
'Carl',
|
||||
'Kyle',
|
||||
'Arthur',
|
||||
'Gerald',
|
||||
'Lawrence',
|
||||
'Roger',
|
||||
'Albert',
|
||||
'Keith',
|
||||
'Jeremy',
|
||||
'Terry',
|
||||
'Joe',
|
||||
'Sean',
|
||||
'Willie',
|
||||
'Jesse',
|
||||
'Ralph',
|
||||
'Billy',
|
||||
'Austin',
|
||||
'Bruce',
|
||||
'Christian',
|
||||
'Roy',
|
||||
'Bryan',
|
||||
'Eugene',
|
||||
'Louis',
|
||||
'Harry',
|
||||
'Wayne',
|
||||
'Ethan',
|
||||
'Jordan',
|
||||
'Russell',
|
||||
'Alan',
|
||||
'Philip',
|
||||
'Randy',
|
||||
'Juan',
|
||||
'Howard',
|
||||
'Vincent',
|
||||
'Bobby',
|
||||
'Dylan',
|
||||
'Johnny',
|
||||
'Phillip',
|
||||
'Craig',
|
||||
'Mary',
|
||||
'Patricia',
|
||||
'Elizabeth',
|
||||
'Jennifer',
|
||||
'Linda',
|
||||
'Barbara',
|
||||
'Susan',
|
||||
'Margaret',
|
||||
'Jessica',
|
||||
'Dorothy',
|
||||
'Sarah',
|
||||
'Karen',
|
||||
'Nancy',
|
||||
'Betty',
|
||||
'Lisa',
|
||||
'Sandra',
|
||||
'Helen',
|
||||
'Donna',
|
||||
'Ashley',
|
||||
'Kimberly',
|
||||
'Carol',
|
||||
'Michelle',
|
||||
'Amanda',
|
||||
'Emily',
|
||||
'Melissa',
|
||||
'Laura',
|
||||
'Deborah',
|
||||
'Stephanie',
|
||||
'Rebecca',
|
||||
'Sharon',
|
||||
'Cynthia',
|
||||
'Ruth',
|
||||
'Kathleen',
|
||||
'Anna',
|
||||
'Shirley',
|
||||
'Amy',
|
||||
'Angela',
|
||||
'Virginia',
|
||||
'Brenda',
|
||||
'Pamela',
|
||||
'Catherine',
|
||||
'Katherine',
|
||||
'Nicole',
|
||||
'Christine',
|
||||
'Janet',
|
||||
'Debra',
|
||||
'Carolyn',
|
||||
'Samantha',
|
||||
'Rachel',
|
||||
'Heather',
|
||||
'Maria',
|
||||
'Diane',
|
||||
'Frances',
|
||||
'Joyce',
|
||||
'Julie',
|
||||
'Martha',
|
||||
'Joan',
|
||||
'Evelyn',
|
||||
'Kelly',
|
||||
'Christina',
|
||||
'Emma',
|
||||
'Lauren',
|
||||
'Alice',
|
||||
'Judith',
|
||||
'Marie',
|
||||
'Doris',
|
||||
'Ann',
|
||||
'Jean',
|
||||
'Victoria',
|
||||
'Cheryl',
|
||||
'Megan',
|
||||
'Kathryn',
|
||||
'Andrea',
|
||||
'Jacqueline',
|
||||
'Gloria',
|
||||
'Teresa',
|
||||
'Janice',
|
||||
'Sara',
|
||||
'Rose',
|
||||
'Julia',
|
||||
'Hannah',
|
||||
'Theresa',
|
||||
'Judy',
|
||||
'Mildred',
|
||||
'Grace',
|
||||
'Beverly',
|
||||
'Denise',
|
||||
'Marilyn',
|
||||
'Amber',
|
||||
'Danielle',
|
||||
'Brittany',
|
||||
'Diana',
|
||||
'Jane',
|
||||
'Lori',
|
||||
'Olivia',
|
||||
'Tiffany',
|
||||
'Kathy',
|
||||
'Tammy',
|
||||
'Crystal',
|
||||
'Madison',
|
||||
]
|
||||
emails = [
|
||||
'@gmail.com',
|
||||
'@yahoo.com',
|
||||
'@outlook.com',
|
||||
'@hotmail.com',
|
||||
'@mailinator.com',
|
||||
'@poly.edu',
|
||||
'@nyu.edu'
|
||||
]
|
||||
extensions = [
|
||||
'.doc',
|
||||
'.log',
|
||||
'.msg',
|
||||
'.rtf',
|
||||
'.txt',
|
||||
'.wpd',
|
||||
'.wps',
|
||||
'.123',
|
||||
'.csv',
|
||||
'.dat',
|
||||
'.db ',
|
||||
'.dll',
|
||||
'.mdb',
|
||||
'.pps',
|
||||
'.ppt',
|
||||
'.sql',
|
||||
'.wks',
|
||||
'.xls',
|
||||
'.xml',
|
||||
'.mng',
|
||||
'.pct',
|
||||
'.bmp',
|
||||
'.gif',
|
||||
'.jpe',
|
||||
'.jpg',
|
||||
'.png',
|
||||
'.psd',
|
||||
'.psp',
|
||||
'.tif',
|
||||
'.ai ',
|
||||
'.drw',
|
||||
'.dxf',
|
||||
'.eps',
|
||||
'.ps ',
|
||||
'.svg',
|
||||
'.3dm',
|
||||
'.3dm',
|
||||
'.ind',
|
||||
'.pdf',
|
||||
'.qxd',
|
||||
'.qxp',
|
||||
'.aac',
|
||||
'.aif',
|
||||
'.iff',
|
||||
'.m3u',
|
||||
'.mid',
|
||||
'.mid',
|
||||
'.mp3',
|
||||
'.mpa',
|
||||
'.ra ',
|
||||
'.ram',
|
||||
'.wav',
|
||||
'.wma',
|
||||
'.3gp',
|
||||
'.asf',
|
||||
'.asx',
|
||||
'.avi',
|
||||
'.mov',
|
||||
'.mp4',
|
||||
'.mpg',
|
||||
'.qt ',
|
||||
'.rm ',
|
||||
'.swf',
|
||||
'.wmv',
|
||||
'.asp',
|
||||
'.css',
|
||||
'.htm',
|
||||
'.htm',
|
||||
'.js ',
|
||||
'.jsp',
|
||||
'.php',
|
||||
'.xht',
|
||||
'.fnt',
|
||||
'.fon',
|
||||
'.otf',
|
||||
'.ttf',
|
||||
'.8bi',
|
||||
'.plu',
|
||||
'.xll',
|
||||
'.cab',
|
||||
'.cpl',
|
||||
'.cur',
|
||||
'.dmp',
|
||||
'.drv',
|
||||
'.key',
|
||||
'.lnk',
|
||||
'.sys',
|
||||
'.cfg',
|
||||
'.ini',
|
||||
'.reg',
|
||||
'.app',
|
||||
'.bat',
|
||||
'.cgi',
|
||||
'.com',
|
||||
'.exe',
|
||||
'.pif',
|
||||
'.vb ',
|
||||
'.ws ',
|
||||
'.deb',
|
||||
'.gz ',
|
||||
'.pkg',
|
||||
'.rar',
|
||||
'.sea',
|
||||
'.sit',
|
||||
'.sit',
|
||||
'.zip',
|
||||
'.bin',
|
||||
'.hqx',
|
||||
'.0 E',
|
||||
'.mim',
|
||||
'.uue',
|
||||
'.cpp',
|
||||
'.jav',
|
||||
'.pl ',
|
||||
'.bak',
|
||||
'.gho',
|
||||
'.old',
|
||||
'.ori',
|
||||
'.tmp',
|
||||
'.dmg',
|
||||
'.iso',
|
||||
'.toa',
|
||||
'.vcd',
|
||||
'.gam',
|
||||
'.nes',
|
||||
'.rom',
|
||||
'.sav',
|
||||
'.msi',
|
||||
'.tor',
|
||||
'.yps'
|
||||
]
|
||||
|
||||
def gen_sentence():
|
||||
return ' '.join(random.sample(lorems, 50))
|
||||
|
||||
def gen_name():
|
||||
return random.choice(names)
|
||||
|
||||
def gen_email():
|
||||
return random.choice(emails)
|
||||
|
||||
def gen_category():
|
||||
return random.choice(categories)
|
||||
|
||||
def gen_value():
|
||||
return random.choice(range(100, 500, 50))
|
||||
|
||||
def gen_word():
|
||||
return random.choice(hipsters)
|
||||
|
||||
def gen_file():
|
||||
return gen_word()+random.choice(extensions)
|
||||
|
||||
def random_date(start, end):
|
||||
return start + datetime.timedelta(
|
||||
seconds=randint(0, int((end - start).total_seconds())))
|
||||
with app.app_context():
|
||||
db = app.db
|
||||
|
||||
### Generating Challenges
|
||||
print "GENERATING CHALLENGES"
|
||||
for x in range(CHAL_AMOUNT):
|
||||
word = gen_word()
|
||||
db.session.add(Challenges(word, gen_sentence(), gen_value(), gen_category()))
|
||||
db.session.commit()
|
||||
db.session.add(Keys(x+1, word, 0))
|
||||
db.session.commit()
|
||||
|
||||
### Generating Files
|
||||
print "GENERATING FILES"
|
||||
AMT_CHALS_WITH_FILES = int(CHAL_AMOUNT * (3.0/4.0))
|
||||
for x in range(AMT_CHALS_WITH_FILES):
|
||||
chal = random.randint(1, CHAL_AMOUNT)
|
||||
filename = gen_file()
|
||||
md5hash = hashlib.md5(filename).hexdigest()
|
||||
db.session.add( Files(chal, os.path.join('static/uploads', md5hash, filename)) )
|
||||
db.session.commit()
|
||||
|
||||
### Generating Users
|
||||
print "GENERATING USERS"
|
||||
used = []
|
||||
while len(used) < USER_AMOUNT:
|
||||
name = gen_name()
|
||||
if name not in used:
|
||||
used.append(name)
|
||||
db.session.add( Teams(name , name.lower() + gen_email(), 'password') )
|
||||
db.session.commit()
|
||||
|
||||
### Generating Solves
|
||||
print "GENERATING SOLVES"
|
||||
base_time = datetime.datetime.utcnow() + datetime.timedelta(minutes=-2880)
|
||||
for x in range(USER_AMOUNT):
|
||||
used = []
|
||||
for y in range(random.randint(1,CHAL_AMOUNT)):
|
||||
chalid = random.randint(1,CHAL_AMOUNT)
|
||||
if chalid not in used:
|
||||
used.append(chalid)
|
||||
solve = Solves(chalid, x+1, '127.0.0.1')
|
||||
|
||||
new_base = random_date(base_time, base_time + datetime.timedelta(minutes=60))
|
||||
solve.date = new_base
|
||||
base_time = new_base
|
||||
|
||||
db.session.add(solve)
|
||||
db.session.commit()
|
||||
|
||||
### Generating Wrong Keys
|
||||
print "GENERATING WRONG KEYS"
|
||||
base_time = datetime.datetime.utcnow() + datetime.timedelta(minutes=-3880)
|
||||
for x in range(USER_AMOUNT):
|
||||
used = []
|
||||
for y in range(random.randint(1,CHAL_AMOUNT * 20)):
|
||||
chalid = random.randint(1,CHAL_AMOUNT)
|
||||
if chalid not in used:
|
||||
used.append(chalid)
|
||||
wrong = WrongKeys(x+1, chalid, gen_word())
|
||||
|
||||
new_base = random_date(base_time, base_time + datetime.timedelta(minutes=60))
|
||||
wrong.date = new_base
|
||||
base_time = new_base
|
||||
|
||||
db.session.add(wrong)
|
||||
db.session.commit()
|
||||
db.session.close()
|
|
@ -0,0 +1,2 @@
|
|||
sudo apt-get install build-essential python-dev python-pip
|
||||
sudo pip install Flask Flask-Session Flask-SQLAlchemy Flask-Mail apscheduler passlib
|
|
@ -0,0 +1,3 @@
|
|||
from CTFd import create_app
|
||||
app = create_app('')
|
||||
app.run(debug=True, host="0.0.0.0", port=4000)
|
|
@ -0,0 +1,84 @@
|
|||
#submit-key{
|
||||
display: none;
|
||||
}
|
||||
|
||||
#chal > h1{
|
||||
text-align: center
|
||||
}
|
||||
|
||||
#chal > form{
|
||||
width: 400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
#chal > form > h3,h4{
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#chal > form > input{
|
||||
display: none;
|
||||
}
|
||||
|
||||
table{
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/*Not sure why foundation needs these two...*/
|
||||
.top-bar input{
|
||||
height: auto;
|
||||
padding-top: 0.35rem;
|
||||
padding-bottom: 0.35rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.top-bar .button{
|
||||
padding-top: 0.45rem;
|
||||
padding-bottom: 0.35rem;
|
||||
margin-bottom: 0;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.dropdown{
|
||||
background-color: #333 !important;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.dropdown button{
|
||||
padding-top: 0.45rem;
|
||||
padding-bottom: 0.35rem;
|
||||
margin-bottom: 0;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
#challenges button{
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.row h1{
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
.textbox{
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
.chal-tag{
|
||||
margin: 0 5px 0 5px;
|
||||
}
|
||||
|
||||
#score-graph{
|
||||
max-height: 400px;
|
||||
}
|
||||
|
||||
#keys-pie-graph{
|
||||
width: 400px;
|
||||
max-height: 330px;
|
||||
float: left;
|
||||
}
|
||||
|
||||
#categories-pie-graph{
|
||||
width: 600px;
|
||||
float: left;
|
||||
max-height: 330px;
|
||||
}
|
|
@ -0,0 +1,197 @@
|
|||
function loadchal(id) {
|
||||
// $('#chal *').show()
|
||||
// $('#chal > h1').hide()
|
||||
obj = $.grep(challenges['game'], function (e) {
|
||||
return e.id == id;
|
||||
})[0]
|
||||
$('#update-challenge .chal-name').val(obj.name)
|
||||
$('#update-challenge .chal-desc').html(obj.description)
|
||||
$('#update-challenge .chal-value').val(obj.value)
|
||||
$('#update-challenge .chal-category').val(obj.category)
|
||||
$('#update-challenge .chal-id').val(obj.id)
|
||||
$('#update-challenge .chal-delete').attr({
|
||||
'href': '/admin/chal/close/' + (id + 1)
|
||||
})
|
||||
$('#update-challenge').foundation('reveal', 'open');
|
||||
|
||||
}
|
||||
|
||||
function submitkey(chal, key) {
|
||||
$.post("/admin/chal/" + chal, {
|
||||
key: key,
|
||||
nonce: $('#nonce').val()
|
||||
}, function (data) {
|
||||
alert(data)
|
||||
})
|
||||
}
|
||||
|
||||
function loadkeys(chal){
|
||||
$.get('/admin/keys/' + chal, function(data){
|
||||
$('#keys-chal').val(chal)
|
||||
keys = $.parseJSON(JSON.stringify(data));
|
||||
keys = keys['keys']
|
||||
$('#current-keys').empty()
|
||||
for(x=0; x<keys.length; x++){
|
||||
$('#current-keys').append($("<input class='current-key' type='text'>").val(keys[x].key))
|
||||
$('#current-keys').append('<input type="radio" name="key_type['+x+']" value="0">Static')
|
||||
$('#current-keys').append('<input type="radio" name="key_type['+x+']" value="1">Regex')
|
||||
$('#current-keys input[name="key_type['+x+']"][value="'+keys[x].type+'"]').prop('checked',true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updatekeys(){
|
||||
keys = [];
|
||||
vals = [];
|
||||
chal = $('#keys-chal').val()
|
||||
$('.current-key').each(function(){
|
||||
keys.push($(this).val());
|
||||
})
|
||||
$('#current-keys > input[name*="key_type"]:checked').each(function(){
|
||||
vals.push($(this).val());
|
||||
})
|
||||
$.post('/admin/keys/'+chal, {'keys':keys, 'vals':vals, 'nonce': $('#nonce').val()})
|
||||
loadchal(chal)
|
||||
}
|
||||
|
||||
function loadtags(chal){
|
||||
$('#tags-chal').val(chal)
|
||||
$('#current-tags').empty()
|
||||
$('#chal-tags').empty()
|
||||
$.get('/admin/tags/'+chal, function(data){
|
||||
tags = $.parseJSON(JSON.stringify(data))
|
||||
tags = tags['tags']
|
||||
for (var i = 0; i < tags.length; i++) {
|
||||
tag = "<span class='secondary label chal-tag'><span>"+tags[i].tag+"</span><a name='"+tags[i].id+"'' class='delete-tag'>×</a></span>"
|
||||
$('#current-tags').append(tag)
|
||||
};
|
||||
$('.delete-tag').click(function(e){
|
||||
deletetag(e.target.name)
|
||||
$(e.target).parent().remove()
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function deletetag(tagid){
|
||||
$.post('/admin/tags/'+tagid+'/delete', {'nonce': $('#nonce').val()});
|
||||
}
|
||||
|
||||
function updatetags(){
|
||||
tags = [];
|
||||
chal = $('#tags-chal').val()
|
||||
$('#chal-tags > span > span').each(function(i, e){
|
||||
tags.push($(e).text())
|
||||
});
|
||||
$.post('/admin/tags/'+chal, {'tags':tags, 'nonce': $('#nonce').val()})
|
||||
loadchal(chal)
|
||||
}
|
||||
|
||||
function loadfiles(chal){
|
||||
$('#update-files > form').attr('action', '/admin/files/'+chal)
|
||||
$.get('/admin/files/' + chal, function(data){
|
||||
$('#files-chal').val(chal)
|
||||
files = $.parseJSON(JSON.stringify(data));
|
||||
files = files['files']
|
||||
$('#current-files').empty()
|
||||
for(x=0; x<files.length; x++){
|
||||
filename = files[x].file.split('/')
|
||||
filename = filename[filename.length - 1]
|
||||
$('#current-files').append('<div data-alert class="alert-box info radius">'+filename+'<a href="#" onclick="deletefile('+chal+','+files[x].id+', $(this))" value="'+files[x].id+'" style="float:right;">Delete</a></div>')
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function deletefile(chal, file, elem){
|
||||
$.post('/admin/files/' + chal,{
|
||||
'nonce': $('#nonce').val(),
|
||||
'method': 'delete',
|
||||
'file': file
|
||||
}, function (data){
|
||||
if (data == "1") {
|
||||
elem.parent().remove()
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function loadchals(){
|
||||
$.post("/admin/chals", {
|
||||
'nonce': $('#nonce').val()
|
||||
}, function (data) {
|
||||
categories = [];
|
||||
challenges = $.parseJSON(JSON.stringify(data));
|
||||
|
||||
|
||||
for (var i = challenges['game'].length - 1; i >= 0; i--) {
|
||||
if ($.inArray(challenges['game'][i].category, categories) == -1) {
|
||||
categories.push(challenges['game'][i].category)
|
||||
$('#challenges').append($('<tr id="' + challenges['game'][i].category.replace(/ /g,"-") + '"><td class="large-2"><h3>' + challenges['game'][i].category + '</h3></td></tr>'))
|
||||
}
|
||||
};
|
||||
|
||||
for (var i = categories.length - 1; i >= 0; i--) {
|
||||
$('#new-challenge select').append('<option value="' + categories[i] + '">' + categories[i] + '</option>');
|
||||
$('#update-challenge select').append('<option value="' + categories[i] + '">' + categories[i] + '</option>');
|
||||
};
|
||||
|
||||
for (var i = 0; i <= challenges['game'].length - 1; i++) {
|
||||
$('#' + challenges['game'][i].category.replace(/ /g,"-")).append($('<button value="' + challenges['game'][i].id + '">' + challenges['game'][i].value + '</button>'));
|
||||
};
|
||||
|
||||
$('#challenges button').click(function (e) {
|
||||
loadchal(this.value);
|
||||
loadkeys(this.value);
|
||||
loadtags(this.value);
|
||||
loadfiles(this.value);
|
||||
});
|
||||
|
||||
$('tr').append('<button class="create-challenge"><i class="fa fa-plus"></i></button>');
|
||||
|
||||
$('.create-challenge').click(function (e) {
|
||||
$('#new-chal-category').val($($(this).siblings()[0]).text().trim())
|
||||
$('#new-chal-title').text($($(this).siblings()[0]).text().trim())
|
||||
$('#new-challenge').foundation('reveal', 'open');
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
$('#submit-key').click(function (e) {
|
||||
submitkey($('#chalid').val(), $('#answer').val())
|
||||
});
|
||||
|
||||
$('#submit-keys').click(function (e) {
|
||||
if (confirm('Updating keys. Are you sure?')){
|
||||
updatekeys()
|
||||
}
|
||||
});
|
||||
|
||||
$('#submit-tags').click(function (e) {
|
||||
updatetags()
|
||||
});
|
||||
|
||||
$(".tag-insert").keyup(function (e) {
|
||||
if (e.keyCode == 13) {
|
||||
tag = $('.tag-insert').val()
|
||||
tag = tag.replace(/'/g, '');
|
||||
if (tag.length > 0){
|
||||
tag = "<span class='secondary label chal-tag'><span>"+tag+"</span><a onclick='$(this).parent().remove()'>×</a></span>"
|
||||
$('#chal-tags').append(tag)
|
||||
}
|
||||
$('.tag-insert').val("")
|
||||
}
|
||||
});
|
||||
|
||||
$('.create-category').click(function (e) {
|
||||
$('#new-category').foundation('reveal', 'open');
|
||||
});
|
||||
count = 1
|
||||
$('#create-key').click(function (e) {
|
||||
$('#current-keys').append("<input class='current-key' type='text' placeholder='Blank Key'>");
|
||||
$('#current-keys').append('<input type="radio" name="key_type['+count+']" value="0">Static');
|
||||
$('#current-keys').append('<input type="radio" name="key_type['+count+']" value="1">Regex');
|
||||
count++
|
||||
});
|
||||
|
||||
$(function(){
|
||||
loadchals()
|
||||
})
|
|
@ -0,0 +1,159 @@
|
|||
function teamid (){
|
||||
loc = window.location.pathname
|
||||
return loc.substring(loc.lastIndexOf('/')+1, loc.length);
|
||||
}
|
||||
|
||||
|
||||
function cumulativesum (arr) {
|
||||
var result = arr.concat();
|
||||
for (var i = 0; i < arr.length; i++){
|
||||
result[i] = arr.slice(0, i + 1).reduce(function(p, i){ return p + i; });
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function scoregraph () {
|
||||
var times = []
|
||||
var scores = []
|
||||
var teamname = $('#team-id').text()
|
||||
$.get('/admin/solves/'+teamid(), function( data ) {
|
||||
solves = $.parseJSON(JSON.stringify(data));
|
||||
solves = solves['solves']
|
||||
|
||||
if (solves.length == 0)
|
||||
return
|
||||
|
||||
for (var i = 0; i < solves.length; i++) {
|
||||
times.push(solves[i].time * 1000)
|
||||
scores.push(solves[i].value)
|
||||
};
|
||||
scores = cumulativesum(scores)
|
||||
|
||||
times.unshift('x1')
|
||||
times.push( Math.round(new Date().getTime()) )
|
||||
|
||||
scores.unshift('data1')
|
||||
scores.push( scores[scores.length-1] )
|
||||
|
||||
var chart = c3.generate({
|
||||
bindto: "#score-graph",
|
||||
data: {
|
||||
xs: {
|
||||
"data1": 'x1',
|
||||
},
|
||||
columns: [
|
||||
times,
|
||||
scores,
|
||||
],
|
||||
type: "area",
|
||||
labels: true,
|
||||
names : {
|
||||
data1: teamname
|
||||
}
|
||||
},
|
||||
axis : {
|
||||
x : {
|
||||
tick: {
|
||||
format: function (x) {
|
||||
return moment(x).local().format('LLL');
|
||||
}
|
||||
},
|
||||
|
||||
},
|
||||
y:{
|
||||
label: {
|
||||
text: 'Score'
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function adjust_times () {
|
||||
$.each($(".solve-time"), function(i, e){
|
||||
$(e).text( moment(parseInt(e.innerText)).local().format('LLL') )
|
||||
})
|
||||
$(".solve-time").css('color', "#222")
|
||||
}
|
||||
|
||||
|
||||
function keys_percentage_graph(){
|
||||
// Solves and Fails pie chart
|
||||
$.get('/admin/fails/'+teamid(), function(data){
|
||||
res = $.parseJSON(JSON.stringify(data));
|
||||
solves = res['solves']
|
||||
fails = res['fails']
|
||||
total = solves+fails
|
||||
|
||||
if (total == 0)
|
||||
return
|
||||
|
||||
var chart = c3.generate({
|
||||
bindto: '#keys-pie-graph',
|
||||
data: {
|
||||
columns: [
|
||||
['Solves', solves],
|
||||
['Fails', fails],
|
||||
],
|
||||
type : 'donut'
|
||||
},
|
||||
color: {
|
||||
pattern: ["#00D140", "#CF2600"]
|
||||
},
|
||||
donut: {
|
||||
title: "Solves vs Fails",
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function category_breakdown_graph(){
|
||||
$.get('/admin/solves/'+teamid(), function(data){
|
||||
solves = $.parseJSON(JSON.stringify(data));
|
||||
solves = solves['solves']
|
||||
|
||||
if (solves.length == 0)
|
||||
return
|
||||
|
||||
categories = []
|
||||
for (var i = 0; i < solves.length; i++) {
|
||||
categories.push(solves[i].category)
|
||||
};
|
||||
|
||||
keys = categories.filter(function(elem, pos) {
|
||||
return categories.indexOf(elem) == pos;
|
||||
})
|
||||
|
||||
data = []
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
temp = []
|
||||
count = 0
|
||||
for (var x = 0; x < categories.length; x++) {
|
||||
if (categories[x] == keys[i]){
|
||||
count++
|
||||
}
|
||||
};
|
||||
temp.push(keys[i])
|
||||
temp.push(count)
|
||||
data.push(temp)
|
||||
};
|
||||
|
||||
var chart = c3.generate({
|
||||
bindto: '#categories-pie-graph',
|
||||
data: {
|
||||
columns: data,
|
||||
type : 'donut',
|
||||
labels: true
|
||||
},
|
||||
donut: {
|
||||
title: "Category Breakdown"
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
category_breakdown_graph()
|
||||
keys_percentage_graph()
|
||||
adjust_times()
|
||||
scoregraph()
|
|
@ -0,0 +1,105 @@
|
|||
#chal > form{
|
||||
width: 400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.reveal-modal{
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.chal-desc{
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
table{
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/*Not sure why foundation needs these two...*/
|
||||
.top-bar input{
|
||||
height: auto;
|
||||
padding-top: 0.35rem;
|
||||
padding-bottom: 0.35rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.top-bar .button{
|
||||
padding-top: 0.45rem;
|
||||
padding-bottom: 0.35rem;
|
||||
margin-bottom: 0;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.dropdown{
|
||||
background-color: #333 !important;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.dropdown button{
|
||||
padding-top: 0.45rem;
|
||||
padding-bottom: 0.35rem;
|
||||
margin-bottom: 0;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
#challenges button{
|
||||
margin: 8px;
|
||||
}
|
||||
|
||||
.row > h1{
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#challenges{
|
||||
line-height: 66px;
|
||||
}
|
||||
|
||||
#score-graph{
|
||||
max-height: 400px;
|
||||
}
|
||||
|
||||
.dropdown{
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.dropdown input{
|
||||
margin: 5px auto;
|
||||
width: 95%;
|
||||
}
|
||||
|
||||
.dropdown button{
|
||||
margin: 10px auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#keys-pie-graph{
|
||||
width: 50%;
|
||||
max-height: 330px;
|
||||
float: left;
|
||||
}
|
||||
|
||||
#categories-pie-graph{
|
||||
width: 50%;
|
||||
float: left;
|
||||
max-height: 330px;
|
||||
}
|
||||
|
||||
.logo{
|
||||
margin: 0 auto;
|
||||
width: 500px;
|
||||
padding: 50px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 40.063em){
|
||||
.top-bar .dropdown{
|
||||
display: block;
|
||||
padding: 0 15px 5px;
|
||||
width: 200% !important;
|
||||
}
|
||||
}
|
||||
|
||||
.top-bar input{
|
||||
padding: 10px;
|
||||
}
|
||||
|
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
|
@ -0,0 +1,244 @@
|
|||
//http://stackoverflow.com/a/2648463 - wizardry!
|
||||
String.prototype.format = String.prototype.f = function() {
|
||||
var s = this,
|
||||
i = arguments.length;
|
||||
|
||||
while (i--) {
|
||||
s = s.replace(new RegExp('\\{' + i + '\\}', 'gm'), arguments[i]);
|
||||
}
|
||||
return s;
|
||||
};
|
||||
|
||||
var challenges;
|
||||
|
||||
function loadchal(id) {
|
||||
obj = $.grep(challenges['game'], function (e) {
|
||||
return e.id == id;
|
||||
})[0]
|
||||
window.location.hash = obj.name
|
||||
$('#chal-window .chal-name').text(obj.name)
|
||||
$('#chal-window .chal-desc').html(marked(obj.description, {'gfm':true, 'breaks':true}))
|
||||
|
||||
for (var i = 0; i < obj.files.length; i++) {
|
||||
filename = obj.files[i].split('/')
|
||||
filename = filename[filename.length - 1]
|
||||
$('#chal-window .chal-desc').append("<a href='"+obj.files[i]+"'>"+filename+"</a><br/>")
|
||||
};
|
||||
|
||||
$('#chal-window .chal-value').text(obj.value)
|
||||
$('#chal-window .chal-category').text(obj.category)
|
||||
$('#chal-window #chal-id').val(obj.id)
|
||||
$('#chal-window .chal-solves').text(obj.solves + " solves")
|
||||
$('#answer').val("")
|
||||
|
||||
$('pre code').each(function(i, block) {
|
||||
hljs.highlightBlock(block);
|
||||
});
|
||||
$('#chal-window').foundation('reveal', 'open');
|
||||
}
|
||||
|
||||
function loadchalbyname(chalname) {
|
||||
obj = $.grep(challenges['game'], function (e) {
|
||||
return e.name == chalname;
|
||||
})[0]
|
||||
window.location.hash = obj.name
|
||||
$('#chal-window .chal-name').text(obj.name)
|
||||
$('#chal-window .chal-desc').html(marked(obj.description, {'gfm':true, 'breaks':true}))
|
||||
|
||||
for (var i = 0; i < obj.files.length; i++) {
|
||||
filename = obj.files[i].split('/')
|
||||
filename = filename[filename.length - 1]
|
||||
$('#chal-window .chal-desc').append("<a href='"+obj.files[i]+"'>"+filename+"</a><br/>")
|
||||
};
|
||||
|
||||
$('#chal-window .chal-value').text(obj.value)
|
||||
$('#chal-window .chal-category').text(obj.category)
|
||||
$('#chal-window #chal-id').val(obj.id)
|
||||
$('#chal-window .chal-solves').text(obj.solves + " solves")
|
||||
$('#answer').val("")
|
||||
|
||||
$('pre code').each(function(i, block) {
|
||||
hljs.highlightBlock(block);
|
||||
});
|
||||
|
||||
$('#chal-window').foundation('reveal', 'open');
|
||||
}
|
||||
|
||||
|
||||
$("#answer").keyup(function(event){
|
||||
if(event.keyCode == 13){
|
||||
$("#submit-key").click();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
function submitkey(chal, key, nonce) {
|
||||
$.post("/chal/" + chal, {
|
||||
key: key,
|
||||
nonce: nonce
|
||||
}, function (data) {
|
||||
if (data == -1){
|
||||
window.location="/login"
|
||||
}
|
||||
else if (data == 0){ // Incorrect key
|
||||
$('#submit-key').text('Incorrect, sorry')
|
||||
$('#submit-key').css('background-color', 'red')
|
||||
$('#submit-key').prop('disabled', true)
|
||||
}
|
||||
else if (data == 1){ // Challenge Solved
|
||||
$('#submit-key').text('Correct!')
|
||||
$('#submit-key').css('background-color', 'green')
|
||||
$('#submit-key').prop('disabled', true)
|
||||
$('#chal-window .chal-solves').text( (parseInt($('#chal-window .chal-solves').text().split(" ")[0]) + 1 + " solves") )
|
||||
}
|
||||
else if (data == 2){ // Challenge already solved
|
||||
$('#submit-key').text('You already solved this')
|
||||
$('#submit-key').prop('disabled', true)
|
||||
}
|
||||
else if (data == 3){ // Keys per minute too high
|
||||
$('#submit-key').text("You're submitting keys too fast. Slow down.")
|
||||
$('#submit-key').css('background-color', '#e18728')
|
||||
$('#submit-key').prop('disabled', true)
|
||||
}
|
||||
marksolves()
|
||||
updatesolves()
|
||||
setTimeout(function(){
|
||||
$('#submit-key').text('Submit')
|
||||
$('#submit-key').prop('disabled', false)
|
||||
$('#submit-key').css('background-color', '#007095')
|
||||
}, 3000);
|
||||
})
|
||||
}
|
||||
|
||||
function marksolves() {
|
||||
$.get('/solves', function (data) {
|
||||
solves = $.parseJSON(JSON.stringify(data));
|
||||
for (var i = solves['solves'].length - 1; i >= 0; i--) {
|
||||
id = solves['solves'][i].chalid
|
||||
$('#challenges button[value="' + id + '"]').addClass('secondary')
|
||||
$('#challenges button[value="' + id + '"]').css('opacity', '0.3')
|
||||
};
|
||||
if (window.location.hash.length > 0){
|
||||
loadchalbyname(window.location.hash.substring(1))
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updatesolves(){
|
||||
$.get('/chals/solves', function (data) {
|
||||
solves = $.parseJSON(JSON.stringify(data));
|
||||
chals = Object.keys(solves);
|
||||
|
||||
for (var i = 0; i < chals.length; i++) {
|
||||
obj = $.grep(challenges['game'], function (e) {
|
||||
return e.name == chals[i];
|
||||
})[0]
|
||||
obj.solves = solves[chals[i]]
|
||||
};
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
function getsolves(id){
|
||||
$.get('/chal/'+id+'/solves', function (data) {
|
||||
var teams = data['teams'];
|
||||
var box = $('#chal-solves-names');
|
||||
box.empty();
|
||||
for (var i = 0; i < teams.length; i++) {
|
||||
var id = teams[i].id;
|
||||
var name = teams[i].name;
|
||||
var date = moment(teams[i].date).local().format('LLL');
|
||||
box.append('<tr><td><a href="/team/{0}">{1}</td><td>{2}</td></tr>'.format(id, name, date));
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function loadchals() {
|
||||
|
||||
$.get("/chals", function (data) {
|
||||
categories = [];
|
||||
challenges = $.parseJSON(JSON.stringify(data));
|
||||
|
||||
|
||||
for (var i = challenges['game'].length - 1; i >= 0; i--) {
|
||||
challenges['game'][i].solves = 0
|
||||
if ($.inArray(challenges['game'][i].category, categories) == -1) {
|
||||
categories.push(challenges['game'][i].category)
|
||||
$('#challenges').append($('<tr id="' + challenges['game'][i].category.replace(/ /g,"-") + '"><td class="large-2"><h4>' + challenges['game'][i].category + '</h4></td></tr>'))
|
||||
}
|
||||
};
|
||||
|
||||
for (var i = 0; i <= challenges['game'].length - 1; i++) {
|
||||
$('#' + challenges['game'][i].category.replace(/ /g,"-")).append($('<button value="' + challenges['game'][i].id + '">' + challenges['game'][i].value + '</button>'));
|
||||
};
|
||||
updatesolves()
|
||||
marksolves()
|
||||
|
||||
$('#challenges button').click(function (e) {
|
||||
loadchal(this.value);
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
$('#submit-key').click(function (e) {
|
||||
submitkey($('#chal-id').val(), $('#answer').val(), $('#nonce').val())
|
||||
});
|
||||
|
||||
$('.chal-solves').click(function (e) {
|
||||
getsolves($('#chal-id').val())
|
||||
});
|
||||
|
||||
// $.distint(array)
|
||||
// Unique elements in array
|
||||
$.extend({
|
||||
distinct : function(anArray) {
|
||||
var result = [];
|
||||
$.each(anArray, function(i,v){
|
||||
if ($.inArray(v, result) == -1) result.push(v);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
});
|
||||
function colorhash (x) {
|
||||
color = ""
|
||||
for (var i = 20; i <= 60; i+=20){
|
||||
x += i
|
||||
x *= i
|
||||
color += x.toString(16)
|
||||
};
|
||||
return "#" + color.substring(0, 6)
|
||||
}
|
||||
|
||||
$(document).on('close', '[data-reveal]', function () {
|
||||
window.location.hash = ""
|
||||
});
|
||||
|
||||
// function solves_graph() {
|
||||
// $.get('/graphs/solves', function(data){
|
||||
// solves = $.parseJSON(JSON.stringify(data));
|
||||
// chals = []
|
||||
// counts = []
|
||||
// colors = []
|
||||
// i = 1
|
||||
// $.each(solves, function(key, value){
|
||||
// chals.push(key)
|
||||
// counts.push(value)
|
||||
// colors.push(colorhash(i++))
|
||||
// });
|
||||
|
||||
// });
|
||||
// }
|
||||
|
||||
function update(){
|
||||
$('#challenges').empty()
|
||||
loadchals()
|
||||
solves_graph()
|
||||
}
|
||||
|
||||
$(function() {
|
||||
loadchals()
|
||||
// solves_graph()
|
||||
});
|
||||
|
||||
setInterval(update, 300000);
|
|
@ -0,0 +1,109 @@
|
|||
//http://stackoverflow.com/a/2648463 - wizardry!
|
||||
String.prototype.format = String.prototype.f = function() {
|
||||
var s = this,
|
||||
i = arguments.length;
|
||||
|
||||
while (i--) {
|
||||
s = s.replace(new RegExp('\\{' + i + '\\}', 'gm'), arguments[i]);
|
||||
}
|
||||
return s;
|
||||
};
|
||||
|
||||
function updatescores () {
|
||||
$.get('/scores', function( data ) {
|
||||
teams = $.parseJSON(JSON.stringify(data));
|
||||
$('#scoreboard > tbody').empty()
|
||||
for (var i = 0; i < teams['teams'].length; i++) {
|
||||
row = "<tr><td>{0}</td><td><a href='/team/{1}'>{2}</a></td><td>{3}</td></tr>".format(i+1, teams['teams'][i].id, teams['teams'][i].name, teams['teams'][i].score)
|
||||
$('#scoreboard > tbody').append(row)
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function cumulativesum (arr) {
|
||||
var result = arr.concat();
|
||||
for (var i = 0; i < arr.length; i++){
|
||||
result[i] = arr.slice(0, i + 1).reduce(function(p, i){ return p + i; });
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function UTCtoDate(utc){
|
||||
var d = new Date(0)
|
||||
d.setUTCSeconds(utc)
|
||||
return d;
|
||||
}
|
||||
|
||||
function scoregraph () {
|
||||
var times = []
|
||||
var scores = []
|
||||
$.get('/top/10', function( data ) {
|
||||
scores = $.parseJSON(JSON.stringify(data));
|
||||
scores = scores['scores']
|
||||
if (Object.keys(scores).length == 0 ){
|
||||
return;
|
||||
}
|
||||
$('#score-graph').show()
|
||||
teams = Object.keys(scores)
|
||||
|
||||
xs_data = {}
|
||||
column_data = []
|
||||
for (var i = 0; i < teams.length; i++) {
|
||||
times = []
|
||||
team_scores = []
|
||||
for (var x = 0; x < scores[teams[i]].length; x++) {
|
||||
times.push(scores[teams[i]][x].time)
|
||||
team_scores.push(scores[teams[i]][x].value)
|
||||
};
|
||||
team_scores = cumulativesum(team_scores)
|
||||
|
||||
times.unshift("x"+i)
|
||||
times.push( Math.round(new Date().getTime()/1000) )
|
||||
|
||||
team_scores.unshift(teams[i])
|
||||
team_scores.push( team_scores[team_scores.length-1] )
|
||||
|
||||
|
||||
xs_data[teams[i]] = "x"+i
|
||||
column_data.push(times)
|
||||
column_data.push(team_scores)
|
||||
|
||||
};
|
||||
|
||||
var chart = c3.generate({
|
||||
bindto: "#score-graph",
|
||||
data: {
|
||||
xs: xs_data,
|
||||
columns: column_data,
|
||||
type: "step",
|
||||
labels: true
|
||||
},
|
||||
axis : {
|
||||
x : {
|
||||
tick: {
|
||||
format: function (x) {
|
||||
return moment(x*1000).local().format('LLL');
|
||||
}
|
||||
},
|
||||
|
||||
},
|
||||
y:{
|
||||
label: {
|
||||
text: 'Score'
|
||||
}
|
||||
}
|
||||
},
|
||||
// zoom : {
|
||||
// enabled: true
|
||||
// }
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function update(){
|
||||
updatescores()
|
||||
scoregraph()
|
||||
}
|
||||
|
||||
setInterval(update, 300000); // Update scores every 5 minutes
|
||||
scoregraph()
|
|
@ -0,0 +1,168 @@
|
|||
function teamid (){
|
||||
loc = window.location.pathname
|
||||
return loc.substring(loc.lastIndexOf('/')+1, loc.length);
|
||||
}
|
||||
|
||||
function colorhash (x) {
|
||||
color = ""
|
||||
for (var i = 20; i <= 60; i+=20){
|
||||
x += i
|
||||
x *= i
|
||||
color += x.toString(16)
|
||||
};
|
||||
return "#" + color.substring(0, 6)
|
||||
}
|
||||
|
||||
function cumulativesum (arr) {
|
||||
var result = arr.concat();
|
||||
for (var i = 0; i < arr.length; i++){
|
||||
result[i] = arr.slice(0, i + 1).reduce(function(p, i){ return p + i; });
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function scoregraph () {
|
||||
var times = []
|
||||
var scores = []
|
||||
var teamname = $('#team-id').text()
|
||||
$.get('/solves/'+teamid(), function( data ) {
|
||||
solves = $.parseJSON(JSON.stringify(data));
|
||||
solves = solves['solves']
|
||||
|
||||
console.log(solves)
|
||||
|
||||
if (solves.length == 0)
|
||||
return
|
||||
|
||||
for (var i = 0; i < solves.length; i++) {
|
||||
times.push(solves[i].time * 1000)
|
||||
scores.push(solves[i].value)
|
||||
};
|
||||
scores = cumulativesum(scores)
|
||||
|
||||
times.unshift('x1')
|
||||
// times.push( Math.round(new Date().getTime()) )
|
||||
|
||||
scores.unshift('data1')
|
||||
// scores.push( scores[scores.length-1] )
|
||||
|
||||
console.log(scores)
|
||||
|
||||
var chart = c3.generate({
|
||||
bindto: "#score-graph",
|
||||
data: {
|
||||
xs: {
|
||||
"data1": 'x1',
|
||||
},
|
||||
columns: [
|
||||
times,
|
||||
scores,
|
||||
],
|
||||
type: "area-step",
|
||||
colors: {
|
||||
data1: colorhash(teamid()),
|
||||
},
|
||||
labels: true,
|
||||
names : {
|
||||
data1: teamname
|
||||
}
|
||||
},
|
||||
axis : {
|
||||
x : {
|
||||
tick: {
|
||||
format: function (x) {
|
||||
return moment(x).local().format('M/D h:mm:ss');
|
||||
}
|
||||
},
|
||||
|
||||
},
|
||||
y:{
|
||||
label: {
|
||||
text: 'Score'
|
||||
}
|
||||
}
|
||||
},
|
||||
zoom : {
|
||||
enabled: true
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function keys_percentage_graph(){
|
||||
// Solves and Fails pie chart
|
||||
$.get('/fails/'+teamid(), function(data){
|
||||
res = $.parseJSON(JSON.stringify(data));
|
||||
solves = res['solves']
|
||||
fails = res['fails']
|
||||
total = solves+fails
|
||||
|
||||
if (total == 0)
|
||||
return
|
||||
|
||||
var chart = c3.generate({
|
||||
bindto: '#keys-pie-graph',
|
||||
data: {
|
||||
columns: [
|
||||
['Solves', solves],
|
||||
['Fails', fails],
|
||||
],
|
||||
type : 'donut'
|
||||
},
|
||||
color: {
|
||||
pattern: ["#00D140", "#CF2600"]
|
||||
},
|
||||
donut: {
|
||||
title: "Solves vs Fails",
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function category_breakdown_graph(){
|
||||
$.get('/solves/'+teamid(), function(data){
|
||||
solves = $.parseJSON(JSON.stringify(data));
|
||||
solves = solves['solves']
|
||||
if (solves.length == 0)
|
||||
return
|
||||
|
||||
categories = []
|
||||
for (var i = 0; i < solves.length; i++) {
|
||||
categories.push(solves[i].category)
|
||||
};
|
||||
|
||||
keys = categories.filter(function(elem, pos) {
|
||||
return categories.indexOf(elem) == pos;
|
||||
})
|
||||
|
||||
data = []
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
temp = []
|
||||
count = 0
|
||||
for (var x = 0; x < categories.length; x++) {
|
||||
if (categories[x] == keys[i]){
|
||||
count++
|
||||
}
|
||||
};
|
||||
temp.push(keys[i])
|
||||
temp.push(count)
|
||||
data.push(temp)
|
||||
};
|
||||
|
||||
var chart = c3.generate({
|
||||
bindto: '#categories-pie-graph',
|
||||
data: {
|
||||
columns: data,
|
||||
type : 'donut',
|
||||
labels: true
|
||||
},
|
||||
donut: {
|
||||
title: "Category Breakdown",
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
category_breakdown_graph()
|
||||
keys_percentage_graph()
|
||||
scoregraph()
|
|
@ -0,0 +1,54 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title></title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/foundation/5.4.7/css/normalize.min.css" />
|
||||
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/foundation/5.4.7/css/foundation.min.css" />
|
||||
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.css" />
|
||||
<link rel="stylesheet" type="text/css" href="/static/admin/css/style.css">
|
||||
<script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.5.1/moment.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="top-bar" data-topbar>
|
||||
<ul class="title-area">
|
||||
<li class="name">
|
||||
<h1><a href="/">CTF</a></h1>
|
||||
</li>
|
||||
<li class="toggle-topbar menu-icon"><a href="#">Menu</a></li>
|
||||
</ul>
|
||||
|
||||
<section class="top-bar-section">
|
||||
<!-- Right Nav Section -->
|
||||
<ul class="right">
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<!-- Left Nav Section -->
|
||||
<ul class="left">
|
||||
<li><a href="/admin/graphs">Graphs</a></li>
|
||||
<li><a href="/admin/pages">Pages</a></li>
|
||||
<li><a href="/admin/teams">Teams</a></li>
|
||||
<li><a href="/admin/scoreboard">Scoreboard</a></li>
|
||||
<li><a href="/admin/chals">Challenges</a></li>
|
||||
<li><a href="/admin/statistics">Statistics</a></li>
|
||||
<li><a href="/admin/config">Config</a></li>
|
||||
</ul>
|
||||
</section>
|
||||
</nav>
|
||||
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
<script src="//cdnjs.cloudflare.com/ajax/libs/foundation/5.4.7/js/vendor/jquery.js"></script>
|
||||
<script src="//cdnjs.cloudflare.com/ajax/libs/foundation/5.4.7/js/vendor/modernizr.js"></script>
|
||||
<script src="//cdnjs.cloudflare.com/ajax/libs/pagedown/1.0/Markdown.Converter.min.js"></script>
|
||||
<script src="//cdnjs.cloudflare.com/ajax/libs/foundation/5.4.7/js/foundation.min.js"></script>
|
||||
<script src="//cdnjs.cloudflare.com/ajax/libs/foundation/5.4.7/js/foundation/foundation.topbar.min.js"></script>
|
||||
<script>
|
||||
$(document).foundation();
|
||||
</script>
|
||||
{% block scripts %}
|
||||
{% endblock %}
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,131 @@
|
|||
{% extends "admin/base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<input type="hidden" value="{{ nonce }}" id="nonce">
|
||||
<div id="new-category" class="reveal-modal" data-reveal>
|
||||
<form method="POST" action="/admin/chal/new" enctype="multipart/form-data">
|
||||
<h3>New Category</h3>
|
||||
<input name='nonce' type='hidden' value="{{ nonce }}">
|
||||
<input type='text' name='category' placeholder='Category'><br/>
|
||||
<a href="#" data-reveal-id="create-challenge" class="button">Create</a>
|
||||
<a class="close-reveal-modal">×</a>
|
||||
</div>
|
||||
|
||||
<div id="create-challenge" class="reveal-modal" data-reveal>
|
||||
<h3>New Challenge</h3>
|
||||
<input type='text' name='name' placeholder='Name'><br/>
|
||||
<textarea name='desc' placeholder='Description'></textarea><br/>
|
||||
<input type='text' name='value' placeholder='Value'><br/>
|
||||
<input type='text' name='key' placeholder='Key'><br/>
|
||||
<input type="radio" name="key_type[0]" value="0">Static
|
||||
<input type="radio" name="key_type[0]" value="1">Regex
|
||||
<br/>
|
||||
<input type="file" name="files[]" multiple="multiple">
|
||||
|
||||
<button type='submit'>Create</button>
|
||||
<a class="close-reveal-modal">×</a>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div id="new-challenge" class="reveal-modal" data-reveal>
|
||||
<form method="POST" action="/admin/chal/new" enctype="multipart/form-data">
|
||||
<h3><span id="new-chal-title"></span> challenge</h3>
|
||||
<input name='nonce' type='hidden' value="{{ nonce }}">
|
||||
<input type='text' name='name' placeholder='Name'><br/>
|
||||
<textarea class="textbox" name='desc' placeholder='Description'></textarea><br/>
|
||||
<input type='text' name='value' placeholder='Value'><br/>
|
||||
<input id="new-chal-category" type="hidden" name="category">
|
||||
<input type='text' name='key' placeholder='Key'><br/>
|
||||
|
||||
<input type="file" name="files[]" multiple="multiple">
|
||||
|
||||
<br/>
|
||||
<input type="radio" name="key_type[0]" value="0">Static
|
||||
<input type="radio" name="key_type[0]" value="1">Regex
|
||||
<br/>
|
||||
|
||||
<button type='submit'>Create</button>
|
||||
<a class="close-reveal-modal">×</a>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div id="update-keys" class="reveal-modal" data-reveal>
|
||||
<form method="POST" action="/admin/keys">
|
||||
<h3>Keys</h3>
|
||||
<input name='nonce' type='hidden' value="{{ nonce }}">
|
||||
<input id="keys-chal" name='chal' type='hidden'>
|
||||
<div id="current-keys"></div>
|
||||
<a href="#" id="create-key" class="secondary button">New Key</a>
|
||||
<a href="#" id="submit-keys" class="button">Update</a>
|
||||
<a class="close-reveal-modal">×</a>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div id="update-files" class="reveal-modal" data-reveal>
|
||||
<form method="POST" action="/admin/files" enctype="multipart/form-data">
|
||||
<h3>Files</h3>
|
||||
<input name='nonce' type='hidden' value="{{ nonce }}">
|
||||
<input id="files-chal" name='chal' type='hidden'>
|
||||
|
||||
<div id="current-files"></div>
|
||||
<input type="hidden" name="method" value="upload">
|
||||
<input type="file" name="files[]" multiple="multiple">
|
||||
<button type='submit'>Upload</button>
|
||||
<a class="close-reveal-modal">×</a>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div id="update-tags" class="reveal-modal" data-reveal>
|
||||
<h3>Tags</h3>
|
||||
<input type="text" class="tag-insert" maxlength="80" placeholder="Type tag and press Enter">
|
||||
<input name='nonce' type='hidden' value="{{ nonce }}">
|
||||
<input id="tags-chal" name='chal' type='hidden'>
|
||||
|
||||
<div id="current-tags">
|
||||
|
||||
</div>
|
||||
<br/>
|
||||
<div id="chal-tags">
|
||||
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
<a href="#" id="submit-tags" class="button">Update</a>
|
||||
<a class="close-reveal-modal">×</a>
|
||||
</div>
|
||||
|
||||
<div id="update-challenge" class="reveal-modal" data-reveal>
|
||||
<form method="POST" action="/admin/chal/update">
|
||||
<h3>Update Challenge</h3>
|
||||
<input name='nonce' type='hidden' value="{{ nonce }}">
|
||||
<input class="chal-name" type='text' name='name' placeholder='Name'><br/>
|
||||
<textarea class="chal-desc textbox" name='desc' placeholder='Description'></textarea><br/>
|
||||
<input class="chal-value" type='text' name='value' placeholder='Value'><br/>
|
||||
<select class="chal-category" name="category">
|
||||
<option>-</option>
|
||||
</select>
|
||||
<input class="chal-id" type='hidden' name='id' placeholder='ID'><br/>
|
||||
|
||||
<a href="#" data-reveal-id="update-tags" class="secondary button">Tags</a>
|
||||
<a href="#" data-reveal-id="update-files" class="secondary button">Files</a>
|
||||
<a href="#" data-reveal-id="update-keys" class="secondary button">Keys</a>
|
||||
<button type='submit'>Update</button>
|
||||
<a class="close-reveal-modal">×</a>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row">
|
||||
<h1>CTF</h1>
|
||||
|
||||
<div>
|
||||
<table id='challenges'>
|
||||
</table>
|
||||
<button style="width:100%;" class="create-category">New Category</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="/static/admin/js/chalboard.js"></script>
|
||||
{% endblock %}
|
|
@ -0,0 +1,21 @@
|
|||
{% extends "admin/base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<br>
|
||||
<h1>Config</h1>
|
||||
<form method="POST">
|
||||
<input name='nonce' type='hidden' value="{{ nonce }}">
|
||||
<strong>Start Date:</strong>
|
||||
<input name='start' type='text' placeholder="Start Date (UTC timestamp)" {% if start is defined and start != None %}value="{{ start }}"{% endif %}>
|
||||
|
||||
<strong>End Date:</strong>
|
||||
<input name='end' type='text' placeholder="End Date (UTC timestamp)" {% if end is defined and end != None %}value="{{ end }}"{% endif %}>
|
||||
|
||||
<button class="radius" type='submit'>Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
{% endblock %}
|
|
@ -0,0 +1,39 @@
|
|||
{% extends "admin/base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<link rel="stylesheet" type="text/css" href="//cdnjs.cloudflare.com/ajax/libs/codemirror/4.8.0/codemirror.css">
|
||||
<div class="row">
|
||||
<div class="row">
|
||||
{% for error in errors %}
|
||||
<div class="large-8 large-centered columns">
|
||||
<div data-alert class="alert-box alert radius centered text-center">
|
||||
<span>{{ error }}</span>
|
||||
<a href="#" class="close">×</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<form id="page-edit" method="POST">
|
||||
<br>
|
||||
<strong>Route: </strong><input name='nonce' type='hidden' value="{{ nonce }}">
|
||||
<input class="radius" id="route" type="text" name="route" value="{% if page is defined %}{{ page.route }}{% endif %}" placeholder="Route">
|
||||
<strong>HTML: </strong><textarea id="admin-pages-editor" name="html">{% if page is defined %}{{ page.html }}{% endif %}</textarea><br>
|
||||
<button class="radius" type='submit'>Create</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="//cdnjs.cloudflare.com/ajax/libs/codemirror/4.8.0/codemirror.min.js"></script>
|
||||
<script>
|
||||
var editor = CodeMirror.fromTextArea(document.getElementById("admin-pages-editor"), {
|
||||
lineNumbers: true,
|
||||
lineWrapping: true,
|
||||
mode: "text/html"
|
||||
});
|
||||
$('#page-edit').submit(function (e){
|
||||
$(this).attr('action', '/admin/pages/'+$('#route').val());
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
|
@ -0,0 +1,168 @@
|
|||
{% extends "admin/base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div id="solves-graph"></div>
|
||||
<div id="keys-pie-graph"></div>
|
||||
<div id="categories-pie-graph"></div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<style>
|
||||
#solves-graph{
|
||||
margin-left: -50px;
|
||||
}
|
||||
#solves-graph > svg{
|
||||
overflow: visible;
|
||||
padding: 10px 50px 0 50px;
|
||||
margin-right: 20px;
|
||||
}
|
||||
text {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
#solves-graph rect {
|
||||
width: 90%;
|
||||
}
|
||||
</style>
|
||||
<script src="//cdnjs.cloudflare.com/ajax/libs/d3/3.4.13/d3.min.js"></script>
|
||||
<script src="//cdnjs.cloudflare.com/ajax/libs/c3/0.4.0/c3.min.js"></script>
|
||||
<script type="text/javascript">
|
||||
// $.distint(array)
|
||||
// Unique elements in array
|
||||
$.extend({
|
||||
distinct : function(anArray) {
|
||||
var result = [];
|
||||
$.each(anArray, function(i,v){
|
||||
if ($.inArray(v, result) == -1) result.push(v);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
});
|
||||
|
||||
// Praise Resig: http://ejohn.org/blog/fast-javascript-maxmin/
|
||||
Array.max = function( array ){
|
||||
return Math.max.apply( Math, array );
|
||||
};
|
||||
|
||||
function colorhash (x) {
|
||||
color = ""
|
||||
for (var i = 20; i <= 60; i+=20){
|
||||
x += i
|
||||
x *= i
|
||||
color += x.toString(16)
|
||||
};
|
||||
return "#" + color.substring(0, 6)
|
||||
}
|
||||
|
||||
function solves_graph() {
|
||||
$.get('/admin/graphs/solves', function(data){
|
||||
solves = $.parseJSON(JSON.stringify(data));
|
||||
chals = []
|
||||
counts = ['Challenges']
|
||||
colors = []
|
||||
i = 1
|
||||
$.each(solves, function(key, value){
|
||||
chals.push(key)
|
||||
counts.push(value)
|
||||
colors.push(colorhash(i++))
|
||||
});
|
||||
|
||||
var chart = c3.generate({
|
||||
bindto: '#solves-graph',
|
||||
size: {
|
||||
width: 1000,
|
||||
height: 500
|
||||
},
|
||||
data: {
|
||||
columns: [
|
||||
counts
|
||||
],
|
||||
type: 'bar',
|
||||
labels: true
|
||||
},
|
||||
axis: {
|
||||
y: {
|
||||
max: null,
|
||||
min: 0,
|
||||
show: false
|
||||
},
|
||||
x: {
|
||||
type: 'categorized',
|
||||
categories: chals,
|
||||
show: false
|
||||
},
|
||||
rotated: true
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function keys_percentage_graph(){
|
||||
// Solves and Fails pie chart
|
||||
$.get('/admin/fails/all', function(data){
|
||||
res = $.parseJSON(JSON.stringify(data));
|
||||
solves = res['solves']
|
||||
fails = res['fails']
|
||||
total = solves+fails
|
||||
|
||||
var chart = c3.generate({
|
||||
bindto: '#keys-pie-graph',
|
||||
data: {
|
||||
columns: [
|
||||
['Solves', solves],
|
||||
['Fails', fails],
|
||||
],
|
||||
type : 'donut'
|
||||
},
|
||||
color: {
|
||||
pattern: ["#00D140", "#CF2600"]
|
||||
},
|
||||
donut: {
|
||||
title: "Solves vs Fails"
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function category_breakdown_graph(){
|
||||
$.get('/admin/graphs/categories', function(data){
|
||||
res = $.parseJSON(JSON.stringify(data));
|
||||
res = res['categories']
|
||||
|
||||
data = []
|
||||
for (var i = 0; i < res.length; i++) {
|
||||
temp = []
|
||||
temp.push(res[i].category)
|
||||
temp.push(res[i].count)
|
||||
data.push(temp)
|
||||
};
|
||||
|
||||
var chart = c3.generate({
|
||||
bindto: '#categories-pie-graph',
|
||||
data: {
|
||||
columns: data,
|
||||
type : 'donut',
|
||||
labels: true
|
||||
},
|
||||
donut: {
|
||||
title: "Category Breakdown"
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function update(){
|
||||
solves_graph()
|
||||
keys_percentage_graph()
|
||||
category_breakdown_graph()
|
||||
}
|
||||
|
||||
$(function() {
|
||||
update()
|
||||
});
|
||||
|
||||
setInterval(update, 300000);
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
|
@ -0,0 +1,29 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<h1>Login</h1>
|
||||
|
||||
<div class="row">
|
||||
{% for error in errors %}
|
||||
<div class="large-8 large-centered columns">
|
||||
<div data-alert class="alert-box alert radius centered text-center">
|
||||
<span>{{ error }}</span>
|
||||
<a href="#" class="close">×</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<form method="POST">
|
||||
<input class="radius" type='text' name='name' placeholder='Name'><br/>
|
||||
<input class="radius" type='password' name='password' placeholder='Password'><br/>
|
||||
<p><a href="/reset_password">Forgot your password?</a></p>
|
||||
<button class="radius" type='submit'>Login</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
{% endblock %}
|
|
@ -0,0 +1,28 @@
|
|||
{% extends "admin/base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<br>
|
||||
<table id="pages">
|
||||
<thead>
|
||||
<tr>
|
||||
<td><b>Route</b></td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for route in routes %}
|
||||
<tr>
|
||||
<td><a href="/admin/pages/{{ route.route }}">{{ route.route }}</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<form method="POST">
|
||||
<input name='nonce' type='hidden' value="{{ nonce }}">
|
||||
<button class="radius" type='submit'>New Page</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
{% endblock %}
|
|
@ -0,0 +1,42 @@
|
|||
{% extends "admin/base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<p></p>
|
||||
<table id="scoreboard">
|
||||
<thead>
|
||||
<tr>
|
||||
<td><b>Place</b></td>
|
||||
<td><b>Team</b></td>
|
||||
<td><b>Score</b></td>
|
||||
<td><b>Status</b></td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for team in teams %}
|
||||
<tr>
|
||||
<td>{{ loop.index }}</td>
|
||||
<td><a href="/admin/team/{{ team.teamid }}">{{ team.name }}</a></td>
|
||||
<td>{{ team.score }}</td>
|
||||
<td>
|
||||
{% if not team.banned %}
|
||||
<form method="POST" style="margin:0;" action="/admin/team/{{ team.teamid }}/ban">
|
||||
<a onclick="$(this).parent().submit()">Ban</a>
|
||||
<input type="hidden" value="{{ nonce }}" name="nonce">
|
||||
</form>
|
||||
{%else %}
|
||||
<form method="POST" style="margin:0;" action="/admin/team/{{ team.teamid }}/unban">
|
||||
<a onclick="$(this).parent().submit()">Unban</a>
|
||||
<input type="hidden" value="{{ nonce }}" name="nonce">
|
||||
</form>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
{% endblock %}
|
|
@ -0,0 +1,88 @@
|
|||
#submit-key{
|
||||
display: none;
|
||||
}
|
||||
|
||||
#chal > h1{
|
||||
text-align: center
|
||||
}
|
||||
|
||||
#chal > form{
|
||||
width: 400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
#chal > form > h3,h4{
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#chal > form > input{
|
||||
display: none;
|
||||
}
|
||||
|
||||
table{
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/*Not sure why foundation needs these two...*/
|
||||
.top-bar input{
|
||||
height: auto;
|
||||
padding-top: 0.35rem;
|
||||
padding-bottom: 0.35rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.top-bar .button{
|
||||
padding-top: 0.45rem;
|
||||
padding-bottom: 0.35rem;
|
||||
margin-bottom: 0;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.dropdown{
|
||||
background-color: #333 !important;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.dropdown button{
|
||||
padding-top: 0.45rem;
|
||||
padding-bottom: 0.35rem;
|
||||
margin-bottom: 0;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
#challenges button{
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.row h1{
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
.textbox{
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
.chal-tag{
|
||||
margin: 0 5px 0 5px;
|
||||
}
|
||||
|
||||
#score-graph{
|
||||
max-height: 400px;
|
||||
}
|
||||
|
||||
#keys-pie-graph{
|
||||
width: 400px;
|
||||
max-height: 330px;
|
||||
float: left;
|
||||
}
|
||||
|
||||
#categories-pie-graph{
|
||||
width: 600px;
|
||||
float: left;
|
||||
max-height: 330px;
|
||||
}
|
||||
|
||||
#admin-pages-editor{
|
||||
width: 100%;
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
{% extends "admin/base.html" %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="row">
|
||||
<h1>Statistics</h1>
|
||||
|
||||
<h3><b>{{ team_count }}</b> teams registered</h3>
|
||||
<h3><b>{{ hit_count}}</b> hits</h3>
|
||||
<h3><b>{{ wrong_count }}</b> wrong keys submitted</h3>
|
||||
<h3><b>{{ solve_count }}</b> right keys submitted</h3>
|
||||
<h3><b>{{ challenge_count }}</b> challenges</h3>
|
||||
{% if most_solved %}
|
||||
<h3>Most solved: <b>{{ most_solved[0].chal.name }}</b> with {{ most_solved[1] }}</b> solves</h3>
|
||||
{% endif %}
|
||||
{% if least_solved %}
|
||||
<h3>Least solved: <b>{{ least_solved[0].chal.name }}</b> with {{ least_solved[1] }}</b> solves</h3>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
{% endblock %}
|
|
@ -0,0 +1,53 @@
|
|||
{% extends "admin/base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<link rel="stylesheet" type="text/css" href="//cdnjs.cloudflare.com/ajax/libs/c3/0.4.0/c3.min.css">
|
||||
|
||||
<div class="row">
|
||||
<h1 id="team-id">{{ team.name }}</h1>
|
||||
|
||||
<div id="keys-pie-graph"></div>
|
||||
<div id="categories-pie-graph"></div>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<td><b>Challenge</b></td>
|
||||
<td><b>Category</b></td>
|
||||
<td><b>Value</b></td>
|
||||
<td><b>Time</b></td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for solve in solves %}
|
||||
<tr><td>{{ solve.chal.name }}</td><td>{{ solve.chal.category }}</td><td>{{ solve.chal.value }}</td><td class="solve-time">{{ solve.date|unix_time_millis }}</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<td><b>IP Address</b></td>
|
||||
<td><b>Last Seen</b></td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for addr in addrs %}
|
||||
<tr><td>{{ addr.ip|long2ip }}</td><td class="solve-time">{{ addr.date|unix_time_millis }}</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div id="score-graph"></div>
|
||||
</div>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.5.1/moment.min.js"></script>
|
||||
<script src="//cdnjs.cloudflare.com/ajax/libs/d3/3.4.13/d3.min.js"></script>
|
||||
<script src="//cdnjs.cloudflare.com/ajax/libs/c3/0.4.0/c3.min.js"></script>
|
||||
<script src="/static/admin/js/team.js"></script>
|
||||
{% endblock %}
|
|
@ -0,0 +1,25 @@
|
|||
{% extends "admin/base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<link rel="stylesheet" type="text/css" href="//cdnjs.cloudflare.com/ajax/libs/c3/0.4.0/c3.min.css">
|
||||
|
||||
<div class="row">
|
||||
<p></p>
|
||||
<table id="teamsboard">
|
||||
<thead>
|
||||
<tr>
|
||||
<td><b>Team</b>
|
||||
</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for team in teams %}
|
||||
<tr><td><a href="/admin/team/{{ team.id }}">{{ team.name }}</a></td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
{% endblock %}
|
|
@ -0,0 +1,93 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title></title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/foundation/5.4.7/css/normalize.min.css" />
|
||||
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/foundation/5.4.7/css/foundation.min.css" />
|
||||
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.css" />
|
||||
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/8.3/styles/railscasts.min.css">
|
||||
<link rel="stylesheet" type="text/css" href="/static/css/style.css">
|
||||
<script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.5.1/moment.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="top-bar" data-topbar>
|
||||
<ul class="title-area">
|
||||
<li class="name">
|
||||
<h1><a href="/">CTF</a></h1>
|
||||
</li>
|
||||
<li class="toggle-topbar menu-icon"><a href="#">Menu</a></li>
|
||||
</ul>
|
||||
|
||||
<section class="top-bar-section">
|
||||
<!-- Right Nav Section -->
|
||||
<ul class="right">
|
||||
{% if username is defined %}
|
||||
<li class="has-dropdown">
|
||||
<a href="/team/{{ id }}">{{ username }} {% if admin %} (ADMIN) {% endif %}</a>
|
||||
<ul class="dropdown">
|
||||
{% if admin %}
|
||||
<li><a href="/admin">Admin</a></li>
|
||||
{% endif %}
|
||||
<li><a href="/team/{{ id }}">Team</a></li>
|
||||
<li><a href="/profile">Account</a></li>
|
||||
<li><a href="/logout">Logout</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
{%else %}
|
||||
<li class="has-form">
|
||||
<li class="has-dropdown">
|
||||
<a href="/register">Register</a>
|
||||
<ul class="dropdown">
|
||||
<form method="POST" action="/register">
|
||||
<li><input type='text' name='name' placeholder='Name'></li>
|
||||
<li><input type='text' name='email' placeholder='Email'></li>
|
||||
<li><input type='password' name='password' placeholder='Password'></li>
|
||||
<li><button type='submit'>Register</button></li>
|
||||
</form>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li class="has-dropdown">
|
||||
<a href="/login">Login</a>
|
||||
<ul class="dropdown">
|
||||
<form method="POST" action="/login">
|
||||
<li><input type='text' name='name' placeholder='Name'></li>
|
||||
<li><input type='password' name='password' placeholder='Password'></li>
|
||||
<li><button type='submit'>Login</button></li>
|
||||
<li><a href="/reset_password" class="text-center">Forgot?</a></li>
|
||||
</form>
|
||||
</ul>
|
||||
</li>
|
||||
{% endif %}
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<!-- Left Nav Section -->
|
||||
<ul class="left">
|
||||
{% for page in pages() %}
|
||||
<li><a href="/{{ page.route }}">{{ page.route|title }}</a></li>
|
||||
{% endfor %}
|
||||
<li><a href="/teams">Teams</a></li>
|
||||
<li><a href="/scoreboard">Scoreboard</a></li>
|
||||
<li><a href="/challenges">Challenges</a></li>
|
||||
</ul>
|
||||
</section>
|
||||
</nav>
|
||||
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
<script src="//cdnjs.cloudflare.com/ajax/libs/foundation/5.4.7/js/vendor/jquery.js"></script>
|
||||
<script src="//cdnjs.cloudflare.com/ajax/libs/foundation/5.4.7/js/vendor/modernizr.js"></script>
|
||||
<script src="//cdnjs.cloudflare.com/ajax/libs/marked/0.3.2/marked.min.js"></script>
|
||||
<script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/8.4/highlight.min.js"></script>
|
||||
<script src="//cdnjs.cloudflare.com/ajax/libs/foundation/5.4.7/js/foundation.min.js"></script>
|
||||
<script src="//cdnjs.cloudflare.com/ajax/libs/foundation/5.4.7/js/foundation/foundation.topbar.min.js"></script>
|
||||
<script>
|
||||
$(document).foundation();
|
||||
</script>
|
||||
{% block scripts %}
|
||||
{% endblock %}
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,54 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<link rel="stylesheet" type="text/css" href="//cdnjs.cloudflare.com/ajax/libs/c3/0.4.0/c3.min.css">
|
||||
|
||||
<div id="chal-solves-window" class="reveal-modal" data-reveal>
|
||||
<h3>Solved By</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<td><b>Name</b>
|
||||
</td>
|
||||
<td><b>Date</b>
|
||||
</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="chal-solves-names">
|
||||
</tbody>
|
||||
</table>
|
||||
<a class="close-reveal-modal" data-reveal-id="chal-window">×</a>
|
||||
</div>
|
||||
|
||||
<div id="chal-window" class="reveal-modal" data-reveal>
|
||||
<div class="row">
|
||||
<h3 class='chal-name'></h3>
|
||||
<h4 class="chal-value"></h4>
|
||||
<p><i><a data-reveal-id="chal-solves-window" class="chal-solves"></a></i></p>
|
||||
<p class="chal-desc"></p>
|
||||
|
||||
<input id="answer" type="text" placeholder="Key">
|
||||
<input type="hidden" id="nonce" name="nonce" value={{ nonce }}>
|
||||
<button id="submit-key">Submit</button>
|
||||
<input id="chal-id" type="hidden">
|
||||
</div>
|
||||
<a class="close-reveal-modal">×</a>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<h2 class="text-center">Challenges</h2>
|
||||
|
||||
<div class="large-12 columns">
|
||||
<table id='challenges'>
|
||||
</table>
|
||||
</div>
|
||||
<div id="solves-graph" style="width: 90%; height: 400px; margin: 0 auto"></div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.5.1/moment.min.js"></script>
|
||||
<script src="//cdnjs.cloudflare.com/ajax/libs/d3/3.4.13/d3.min.js"></script>
|
||||
<script src="//cdnjs.cloudflare.com/ajax/libs/c3/0.4.0/c3.min.js"></script>
|
||||
<script src="/static/js/chalboard.js"></script>
|
||||
{% endblock %}
|
|
@ -0,0 +1 @@
|
|||
CTF
|
|
@ -0,0 +1,14 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="row">
|
||||
<h2 style="text-align:center;">I don't think you're allowed here</h2>
|
||||
<h2 style="text-align:center;">Not so sorry about that</h2>
|
||||
</div>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
{% endblock %}
|
|
@ -0,0 +1,14 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="row">
|
||||
<h2 style="text-align:center;">Whoops, looks like we can't find that.</h2>
|
||||
<h2 style="text-align:center;">Sorry about that</h2>
|
||||
</div>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
{% endblock %}
|
|
@ -0,0 +1,13 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="row">
|
||||
<h2 style="text-align:center;">Uhh what did you just do?</h2>
|
||||
</div>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
{% endblock %}
|
|
@ -0,0 +1,29 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<h1>Login</h1>
|
||||
|
||||
<div class="row">
|
||||
{% for error in errors %}
|
||||
<div class="large-8 large-centered columns">
|
||||
<div data-alert class="alert-box alert radius centered text-center">
|
||||
<span>{{ error }}</span>
|
||||
<a href="#" class="close">×</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<form method="POST">
|
||||
<input class="radius" type='text' name='name' placeholder='Name'><br/>
|
||||
<input class="radius" type='password' name='password' placeholder='Password'><br/>
|
||||
<p><a href="/reset_password">Forgot your password?</a></p>
|
||||
<button class="radius" type='submit'>Login</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
{% endblock %}
|
|
@ -0,0 +1,46 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<link rel="stylesheet" type="text/css" href="//cdnjs.cloudflare.com/ajax/libs/c3/0.4.0/c3.min.css">
|
||||
|
||||
<div class="row">
|
||||
<br/>
|
||||
{% for error in errors %}
|
||||
<h1>{{ error }}</h1>
|
||||
{% endfor %}
|
||||
<form method="POST">
|
||||
<span>Team Name</span>
|
||||
<input class="radius" type="text" name="name" placeholder="Team Name" value="{{name}}">
|
||||
|
||||
<span>Email Address</span>
|
||||
<input class="radius" type="text" name="email" placeholder="Email Address" value="{{email}}">
|
||||
<br/>
|
||||
<hr/>
|
||||
<br/>
|
||||
<span>Old Password</span>
|
||||
<input class="radius" type="password" name="confirm" placeholder="Old Password">
|
||||
|
||||
<span>Password</span>
|
||||
<input class="radius" type="password" name="password" placeholder="Password">
|
||||
<br/>
|
||||
<hr/>
|
||||
<br/>
|
||||
<span>Website</span>
|
||||
<input class="radius" type="text" name="website" placeholder="Website" value="{% if website %}{{website}}{% endif %}">
|
||||
<span>Affiliation</span>
|
||||
<input class="radius" type="text" name="affiliation" placeholder="Affiliation" value="{% if affiliation %}{{affiliation}}{% endif %}">
|
||||
<span>Country</span>
|
||||
<input class="radius" type="text" name="country" placeholder="Country" value="{% if country %}{{country}}{% endif %}">
|
||||
<input type="hidden" name="nonce" value="{{nonce}}">
|
||||
<button class="radius">Update</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="//cdnjs.cloudflare.com/ajax/libs/d3/3.4.13/d3.min.js"></script>
|
||||
<script src="//cdnjs.cloudflare.com/ajax/libs/c3/0.4.0/c3.min.js"></script>
|
||||
<script src="/static/js/team.js"></script>
|
||||
{% endblock %}
|
|
@ -0,0 +1,34 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<h1>Register</h1>
|
||||
|
||||
<div class="row">
|
||||
{% for error in errors %}
|
||||
<div class="large-8 large-centered columns">
|
||||
<div data-alert class="alert-box alert radius centered text-center">
|
||||
<span>{{ error }}</span>
|
||||
<a href="#" class="close">×</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<form method="POST">
|
||||
<input type='text' class="radius" name='name' placeholder='Team Name' {% if name %}value="{{ name }}"{% endif %}><br/>
|
||||
<input type='text' class="radius" name='email' placeholder='Email' {% if email %}value="{{ email }}"{% endif %}><br/>
|
||||
<input type='password' class="radius" name='password' placeholder='Password' {% if password %}value="{{ password }}"{% endif %}><br/>
|
||||
<button class="radius" type='submit'>Register</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
if (window.location.hash == "#frame"){
|
||||
$('.top-bar').hide()
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
|
@ -0,0 +1,34 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<h1>Reset Password</h1>
|
||||
|
||||
<div class="row">
|
||||
{% for error in errors %}
|
||||
<div class="large-8 large-centered columns">
|
||||
<div data-alert class="alert-box alert radius centered text-center">
|
||||
<span>{{ error }}</span>
|
||||
<a href="#" class="close">×</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% if mode %}
|
||||
<form method="POST">
|
||||
<input class="radius" type='password' name='password' placeholder='Password'><br/>
|
||||
<button class="radius" type='submit'>Reset Password</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<form method="POST">
|
||||
<input class="radius" type='text' name='email' placeholder='Email'><br/>
|
||||
<button class="radius" type='submit'>Reset Password</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
{% endblock %}
|
|
@ -0,0 +1,36 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<link rel="stylesheet" type="text/css" href="//cdnjs.cloudflare.com/ajax/libs/c3/0.4.0/c3.min.css">
|
||||
|
||||
<div class="row">
|
||||
<p></p>
|
||||
<div id="score-graph"></div>
|
||||
<p></p>
|
||||
|
||||
<table id="scoreboard">
|
||||
<thead>
|
||||
<tr>
|
||||
<td><b>Place</b>
|
||||
</td>
|
||||
<td><b>Team</b>
|
||||
</td>
|
||||
<td><b>Score</b>
|
||||
</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for team in teams %}
|
||||
<tr><td>{{ loop.index }}</td><td><a href="/team/{{ team.teamid }}">{{ team.name }}</a></td><td>{{ team.score }}</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.5.1/moment.min.js"></script>
|
||||
<script src="//cdnjs.cloudflare.com/ajax/libs/d3/3.4.13/d3.min.js"></script>
|
||||
<script src="//cdnjs.cloudflare.com/ajax/libs/c3/0.4.0/c3.min.js"></script>
|
||||
<script src="/static/js/scoreboard.js"></script>
|
||||
{% endblock %}
|
|
@ -0,0 +1,52 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<link rel="stylesheet" type="text/css" href="//cdnjs.cloudflare.com/ajax/libs/codemirror/4.8.0/codemirror.css">
|
||||
<div class="row">
|
||||
<h1>Setup</h1>
|
||||
|
||||
<div class="row">
|
||||
{% for error in errors %}
|
||||
<div class="large-8 large-centered columns">
|
||||
<div data-alert class="alert-box alert radius centered text-center">
|
||||
<span>{{ error }}</span>
|
||||
<a href="#" class="close">×</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<form method="POST">
|
||||
<strong>Username</strong>
|
||||
<input class="radius" type='text' name='name' placeholder='Name'><br/>
|
||||
|
||||
<strong>Email</strong>
|
||||
<input class="radius" type='text' name='email' placeholder='Email'><br/>
|
||||
|
||||
<strong>Password</strong>
|
||||
<input class="radius" type='password' name='password' placeholder='Password'><br/>
|
||||
<strong>Index Page</strong>
|
||||
<textarea id="pages-editor" name="html">
|
||||
<div class="row">
|
||||
<img class="logo" src="/static/img/logo.png">
|
||||
<h3 class="text-center">Welcome to a cool CTF framwork written by <a href="https://github.com/ColdHeat">Kevin Chung</a> of <a href="https://github.com/isislab">@isislab</a></h3>
|
||||
|
||||
<h4 class="text-center"><a href="/admin">Click here</a> to login and setup your CTF</h4>
|
||||
</div>
|
||||
</textarea><br>
|
||||
<button class="radius" type='submit'>Login</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="//cdnjs.cloudflare.com/ajax/libs/codemirror/4.8.0/codemirror.min.js"></script>
|
||||
<script>
|
||||
var editor = CodeMirror.fromTextArea(document.getElementById("pages-editor"), {
|
||||
lineNumbers: true,
|
||||
lineWrapping: true,
|
||||
mode: "text/html"
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
|
@ -0,0 +1,37 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<link rel="stylesheet" type="text/css" href="//cdnjs.cloudflare.com/ajax/libs/c3/0.4.0/c3.min.css">
|
||||
|
||||
<div class="row">
|
||||
<h1 id="team-id">{{ team.name }}</h1>
|
||||
|
||||
<div id="keys-pie-graph"></div>
|
||||
<div id="categories-pie-graph"></div>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<td><b>Challenge</b></td>
|
||||
<td><b>Category</b></td>
|
||||
<td><b>Value</b></td>
|
||||
<td><b>Time</b></td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for solve in solves %}
|
||||
<tr><td><a href="/challenges#{{ solve.chal.name }}">{{ solve.chal.name }}</a></td><td>{{ solve.chal.category }}</td><td>{{ solve.chal.value }}</td><td class="solve-time"><script>document.write( moment({{ solve.date|unix_time_millis }}).local().format('MMMM Do, h:mm:ss A'))</script></td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<div id="score-graph"></div>
|
||||
</div>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="//cdnjs.cloudflare.com/ajax/libs/d3/3.4.13/d3.min.js"></script>
|
||||
<script src="//cdnjs.cloudflare.com/ajax/libs/c3/0.4.0/c3.min.js"></script>
|
||||
<script src="/static/js/team.js"></script>
|
||||
{% endblock %}
|
|
@ -0,0 +1,32 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<link rel="stylesheet" type="text/css" href="//cdnjs.cloudflare.com/ajax/libs/c3/0.4.0/c3.min.css">
|
||||
|
||||
<div class="row">
|
||||
<p></p>
|
||||
<table id="teamsboard">
|
||||
<thead>
|
||||
<tr>
|
||||
<td><b>Team</b></td>
|
||||
<td><b>Website</b></td>
|
||||
<td><b>Affiliation</b></td>
|
||||
<td><b>Country</b></td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for team in teams %}
|
||||
<tr>
|
||||
<td><a href="/team/{{ team.id }}">{{ team.name }}</a></td>
|
||||
<td><a href="{{ team.website }}">{% if team.website %}{{ team.website }}{% endif %}</a></td>
|
||||
<td><span>{% if team.affiliation %}{{ team.affiliation }}{% endif %}</span></td>
|
||||
<td><span>{% if team.country %}{{ team.country }}{% endif %}</span></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
{% endblock %}
|
Loading…
Reference in New Issue