diff --git a/ctf.nginx b/ctf.nginx index 579eba7..bd36907 100644 --- a/ctf.nginx +++ b/ctf.nginx @@ -13,7 +13,11 @@ server { # } # Put all the pages here so Angular doesn't fail. - location ~^/(about|login|register|scoreboard|chat|updates|problems|programming|shell|rules|admin/problems)$ { + location ~^/(about|chat|help|learn|login|profile|register|scoreboard|settings|team)$ { + default_type text/html; + try_files /index.html /index.html; + } + location ~^/admin/(problems)$ { default_type text/html; try_files /index.html /index.html; } diff --git a/deploy b/deploy index 2d6014c..bb04d8e 100755 --- a/deploy +++ b/deploy @@ -5,7 +5,9 @@ pkill gunicorn sudo service nginx stop tmux kill-session -t ctf 2> /dev/null +sudo cp /vagrant/ctf.nginx /etc/nginx/sites-enabled/ctf + echo "Starting the server..." cd /home/vagrant/server sudo service nginx start -tmux new-session -s ctf -d 'gunicorn "app:app" -c /home/vagrant/scripts/gunicorn.py.ini' +tmux new-session -s ctf -d 'gunicorn "app:app" -c /home/vagrant/scripts/gunicorn.py.ini' \ No newline at end of file diff --git a/scripts/requirements.txt b/scripts/requirements.txt index ec2f247..7d8974f 100644 --- a/scripts/requirements.txt +++ b/scripts/requirements.txt @@ -4,3 +4,4 @@ Flask-SQLAlchemy SQLAlchemy gunicorn requests +voluptuous \ No newline at end of file diff --git a/server/api.html b/server/api.html new file mode 100644 index 0000000..dfb8ca9 --- /dev/null +++ b/server/api.html @@ -0,0 +1,31 @@ + + +Python: package api + + + + + +
 
+ 
api
index
/home/vagrant/server/api/__init__.py
+

+

+ + + + + +
 
+Package Contents
       
admin
+api
+decorators
+
logger
+models
+problem
+
schemas
+user
+utils
+
+ \ No newline at end of file diff --git a/server/api/admin.py b/server/api/admin.py index 5152a88..8af0c3c 100644 --- a/server/api/admin.py +++ b/server/api/admin.py @@ -1,19 +1,26 @@ from flask import Blueprint, jsonify -from decorators import admins_only, api_wrapper, login_required +from decorators import admins_only, api_wrapper from models import db, Problems, Files +import json + blueprint = Blueprint("admin", __name__) -@blueprint.route("/problem/data", methods=["POST"]) -#@api_wrapper # Disable atm due to json serialization issues: will fix +@blueprint.route("/problems/list", methods=["POST"]) +@api_wrapper @admins_only -@login_required def problem_data(): - problems = Problems.query.add_columns("pid", "name", "category", "description", "hint", "value", "solves", "disabled", "flag").order_by(Problems.value).all() - jason = [] - - for problem in problems: - problem_files = [ str(_file.location) for _file in Files.query.filter_by(pid=int(problem.pid)).all() ] - jason.append({"pid": problem[1], "name": problem[2] ,"category": problem[3], "description": problem[4], "hint": problem[5], "value": problem[6], "solves": problem[7], "disabled": problem[8], "flag": problem[9], "files": problem_files}) - - return jsonify(data=jason) + problems = Problems.query.order_by(Problems.value).all() + problems_return = [ ] + for problem in problems: + problems_return.append({ + "pid": problem.pid, + "name": problem.name, + "category": problem.category, + "description": problem.description, + "hint": problem.hint, + "value": problem.value, + "threshold": problem.threshold, + "weightmap": json.loads(problem.weightmap) + }) + return { "success": 1, "problems": problems_return } \ No newline at end of file diff --git a/server/api/decorators.py b/server/api/decorators.py index 59715a0..558f6b3 100644 --- a/server/api/decorators.py +++ b/server/api/decorators.py @@ -19,7 +19,7 @@ def api_wrapper(f): except Exception as error: response = 200 traceback.print_exc() - web_result = { "success": 0, "message": "Something went wrong! Please notify us about this immediately. %s: %s" % (error, traceback.format_exc()) } + web_result = { "success": 0, "message": "Something went wrong! Please notify us about this immediately.", "error": [ str(error), traceback.format_exc() ] } return json.dumps(web_result), response, { "Content-Type": "application/json; charset=utf-8" } return wrapper diff --git a/server/api/models.py b/server/api/models.py index a69ee79..715d880 100644 --- a/server/api/models.py +++ b/server/api/models.py @@ -1,73 +1,104 @@ from flask.ext.sqlalchemy import SQLAlchemy +import time import utils db = SQLAlchemy() class Users(db.Model): - uid = db.Column(db.Integer, primary_key=True) - tid = db.Column(db.Integer) - name = db.Column(db.String(64)) - username = db.Column(db.String(64), unique=True) - username_lower = db.Column(db.String(64), unique=True) - email = db.Column(db.String(64), unique=True) - password = db.Column(db.String(128)) - admin = db.Column(db.Boolean, default=False) + uid = db.Column(db.Integer, unique=True, primary_key=True) + tid = db.Column(db.Integer) + name = db.Column(db.String(64)) + username = db.Column(db.String(64), unique=True) + username_lower = db.Column(db.String(64), unique=True) + email = db.Column(db.String(64), unique=True) + password = db.Column(db.String(128)) + admin = db.Column(db.Boolean) + utype = db.Column(db.Integer) + tid = db.Column(db.Integer) + registertime = db.Column(db.Integer) - def __init__(self, name, username, email, password): - self.name = name - self.username = username - self.username_lower = username.lower() - self.email = email.lower() - self.password = utils.hash_password(password) + def __init__(self, name, username, email, password, utype=1): + self.name = name + self.username = username + self.username_lower = username.lower() + self.email = email.lower() + self.password = utils.hash_password(password) + self.utype = utype + self.admin = False + self.registertime = int(time.time()) class Teams(db.Model): - tid = db.Column(db.Integer, primary_key=True) - name = db.Column(db.String(64), unique=True) - join_code = db.Column(db.String(128), unique=True) - school = db.Column(db.Text) - size = db.Column(db.Integer) - score = db.Column(db.Integer) - observer = db.Column(db.Boolean) - owner = db.Column(db.Integer) + tid = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(64), unique=True) + join_code = db.Column(db.String(128), unique=True) + school = db.Column(db.Text) + size = db.Column(db.Integer) + score = db.Column(db.Integer) + observer = db.Column(db.Boolean) + owner = db.Column(db.Integer) - def __init__(self, name, school): - self.name = name - self.school = school + def __init__(self, name, school): + self.name = name + self.school = school class Problems(db.Model): - pid = db.Column(db.Integer, primary_key=True) - name = db.Column(db.String(128)) - category = db.Column(db.String(128)) - description = db.Column(db.Text) - hint = db.Column(db.Text) - flag = db.Column(db.Text) - disabled = db.Column(db.Boolean, default=False) - value = db.Column(db.Integer) - solves = db.Column(db.Integer, default=0) + pid = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(128)) + category = db.Column(db.String(128)) + description = db.Column(db.Text) + hint = db.Column(db.Text) + flag = db.Column(db.Text) + disabled = db.Column(db.Boolean, default=False) + value = db.Column(db.Integer) + solves = db.Column(db.Integer, default=0) - def __init__(self, name, category, description, hint, flag, value): - self.name = name - self.category = category - self.description = description - self.hint = hint - self.flag = flag - self.value = value + def __init__(self, name, category, description, hint, flag, value): + self.name = name + self.category = category + self.description = description + self.hint = hint + self.flag = flag + self.value = value class Files(db.Model): - fid = db.Column(db.Integer, primary_key=True) - pid = db.Column(db.Integer) - location = db.Column(db.Text) + fid = db.Column(db.Integer, primary_key=True) + pid = db.Column(db.Integer) + location = db.Column(db.Text) - def __init__(self, pid, location): - self.pid = pid - self.location = location + def __init__(self, pid, location): + self.pid = pid + self.location = location class Solves(db.Model): - sid = db.Column(db.Integer, primary_key=True) - pid = db.Column(db.Integer) - tid = db.Column(db.Integer) - date = db.Column(db.Integer, default=utils.get_time_since_epoch()) + sid = db.Column(db.Integer, primary_key=True) + pid = db.Column(db.Integer) + tid = db.Column(db.Integer) + date = db.Column(db.Integer, default=utils.get_time_since_epoch()) - def __init__(self, pid, tid): - self.pid = pid - self.tid = tid + def __init__(self, pid, tid): + self.pid = pid + self.tid = tid + +########## +# TOKENS # +########## + +class LoginTokens(db.Model): + sid = db.Column(db.String(64), unique=True, primary_key=True) + uid = db.Column(db.Integer) + username = db.Column(db.String(32)) + active = db.Column(db.Boolean) + issued = db.Column(db.Integer) + expiry = db.Column(db.Integer) + ua = db.Column(db.String(128)) + ip = db.Column(db.String(16)) + + def __init__(self, uid, username, expiry=int(time.time()), active=True, ua=None, ip=None): + self.sid = utils.generate_string() + self.uid = uid + self.username = username + self.issued = int(time.time()) + self.expiry = expiry + self.active = active + self.ua = ua + self.ip = ip \ No newline at end of file diff --git a/server/api/schemas.py b/server/api/schemas.py new file mode 100644 index 0000000..1648950 --- /dev/null +++ b/server/api/schemas.py @@ -0,0 +1,24 @@ +import api +import re + +from voluptuous import Required, Length, Schema, Invalid, MultipleInvalid +from decorators import WebException + +def check(*callback_tuples): + def v(value): + for callbacks, msg in callback_tuples: + for callback in callbacks: + try: + result = callback(value) + if not result and type(result) == bool: + raise Invalid(msg) + except Exception: + raise WebException(msg) + return value + return v + +def verify_to_schema(schema, data): + try: + schema(data) + except MultipleInvalid as error: + raise WebException(str(error)) \ No newline at end of file diff --git a/server/api/user.py b/server/api/user.py index 08aa15a..bc786c8 100644 --- a/server/api/user.py +++ b/server/api/user.py @@ -1,96 +1,208 @@ from flask import Blueprint, session, request, redirect, url_for from flask import current_app as app +from voluptuous import Schema, Length, Required -from models import db, Users -from decorators import api_wrapper +from models import db, LoginTokens, Users +from decorators import api_wrapper, WebException +from schemas import verify_to_schema, check +import datetime import logger +import re import requests import utils +############### +# USER ROUTES # +############### + blueprint = Blueprint("user", __name__) @blueprint.route("/register", methods=["POST"]) @api_wrapper def user_register(): - # if not validate_captcha(request.form): - # return { "success": 0, "message": "Please do the captcha." } + params = utils.flat_multi(request.form) - name = request.form["name"] - username = request.form["username"] - password = request.form["password"] - password_confirm = request.form["password_confirm"] - email = request.form["email"] + name = params.get("name") + email = params.get("email") + username = params.get("username") + password = params.get("password") + password_confirm = params.get("password_confirm") + utype = int(params.get("type")) - username_exists = Users.query.add_columns("name", "uid").filter_by(username_lower=username.lower()).first() - email_exists = Users.query.add_columns("name", "uid").filter_by(email=email.lower()).first() + if password != password_confirm: + raise WebException("Passwords do not match.") + verify_to_schema(UserSchema, params) - if password != password_confirm: - return { "success": 0, "message": "Passwords do not match." } - if len(password) > 128: - return { "success": 0, "message": "Password is too long." } - if len(password) == 0: - return { "success": 0, "message": "Password is too short." } - if len(username) > 64: - return { "success": 0, "message": "Username is too long." } - if username_exists: - return { "success": 0, "message": "Username is already taken." } - if email_exists: - return { "success": 0, "message": "Email has already been used." } + user = Users(name, username, email, password, utype) + with app.app_context(): + db.session.add(user) + db.session.commit() - add_user(name, username, email, password) - logger.log("registrations", logger.INFO, "%s registered with %s" % (name.encode("utf-8"), email.encode("utf-8"))) + logger.log("registrations", logger.INFO, "%s registered with %s" % (name.encode("utf-8"), email.encode("utf-8"))) + login_user(username, password) - return { "success": 1, "message": "Success!" } + return { "success": 1, "message": "Success!" } @blueprint.route("/logout", methods=["POST"]) @api_wrapper def user_logout(): - session.clear() + sid = session["sid"] + username = session["username"] + with app.app_context(): + expired = LoginTokens.query.filter_by(username=username).all() + for expired_token in expired: db.session.delete(expired_token) + db.session.commit() + session.clear() @blueprint.route("/login", methods=["POST"]) @api_wrapper def user_login(): - email = request.form["email"] - password = request.form["password"] - user = Users.query.filter_by(email=email).first() - if user is None: - return { "success": 0, "message": "Invalid credentials." } + params = utils.flat_multi(request.form) - if utils.check_password(user.password, password): - session["username"] = user.username - if user.admin: - session["admin"] = True - session["logged_in"] = True - return { "success": 1, "message": "Success!" } - else: - return { "success": 0, "message": "Invalid credentials." } + username = params.get("username") + password = params.get("password") + + result = login_user(username, password) + if result != True: + raise WebException("Please check if your username/password are correct.") + + return { "success": 1, "message": "Success!" } @blueprint.route("/status", methods=["POST"]) @api_wrapper def user_status(): - status = { - "logged_in": is_logged_in(), - "admin": is_admin(), - "username": session["username"] if is_logged_in() else "", - } - return status + logged_in = is_logged_in() + result = { + "success": 1, + "logged_in": logged_in, + "admin": is_admin(), + "username": session["username"] if logged_in else "", + } + return result + +@blueprint.route("/info", methods=["POST"]) +@api_wrapper +def user_info(): + logged_in = is_logged_in() + username = utils.flat_multi(request.form).get("username") + if username is None: + if logged_in: + username = session["username"] + if username is None: + raise WebException("No user specified.") + me = username.lower() == session["username"].lower() + user = get_user(username_lower=username.lower()).first() + if user is None: + raise WebException("User not found.") + + show_email = me if logged_in else False + userdata = { + "user_found": True, + "name": user.name, + "username": user.username, + "type": ["Student", "Instructor", "Observer"][user.utype - 1], + "admin": user.admin, + "registertime": datetime.datetime.fromtimestamp(user.registertime).isoformat() + "Z", + "me": me, + "show_email": show_email + } + if show_email: + userdata["email"] = user.email + return { "success": 1, "user": userdata } + +################## +# USER FUNCTIONS # +################## + +__check_email_format = lambda email: re.match(".+@.+\..{2,}", email) is not None +__check_ascii = lambda s: all(ord(c) < 128 for c in s) +__check_username = lambda username: get_user(username_lower=username.lower()).first() is None +__check_email = lambda email: get_user(email=email.lower()).first() is None + +UserSchema = Schema({ + Required("email"): check( + ([str, Length(min=4, max=128)], "Your email should be between 4 and 128 characters long."), + ([__check_email], "Someone already registered this email."), + ([__check_email_format], "Please enter a legit email.") + ), + Required("name"): check( + ([str, Length(min=4, max=128)], "Your name should be between 4 and 128 characters long.") + ), + Required("username"): check( + ([str, Length(min=4, max=32)], "Your username should be between 4 and 32 characters long."), + ([__check_ascii], "Please only use ASCII characters in your username."), + ([__check_username], "This username is taken, did you forget your password?") + ), + Required("password"): check( + ([str, Length(min=4, max=64)], "Your password should be between 4 and 64 characters long."), + ([__check_ascii], "Please only use ASCII characters in your password."), + ), + Required("type"): check( + ([str, lambda x: x.isdigit()], "Please use the online form.") + ), + "notify": str +}, extra=True) + +def get_user(username=None, username_lower=None, email=None, uid=None): + match = {} + if username != None: + match.update({ "username": username }) + elif username_lower != None: + match.update({ "username_lower": username_lower }) + elif uid != None: + match.update({ "uid": uid }) + elif email != None: + match.update({ "email": email }) + # elif api.auth.is_logged_in(): + # match.update({ "uid": api.auth.get_uid() }) + with app.app_context(): + result = Users.query.filter_by(**match) + return result + +def login_user(username, password): + user = get_user(username_lower=username.lower()).first() + if user is None: return False + correct = utils.check_password(user.password, password) + if not correct: return False + + useragent = request.headers.get("User-Agent") + ip = request.remote_addr + + with app.app_context(): + expired = LoginTokens.query.filter_by(username=username).all() + for expired_token in expired: db.session.delete(expired_token) + + token = LoginTokens(user.uid, user.username, ua=useragent, ip=ip) + db.session.add(token) + db.session.commit() + + session["sid"] = token.sid + session["username"] = token.username + session["admin"] = user.admin == True + + return True def is_logged_in(): - return "logged_in" in session and session["logged_in"] + sid = session["sid"] + username = session["username"] + token = LoginTokens.query.filter_by(sid=sid).first() + if token is None: return False + + useragent = request.headers.get("User-Agent") + ip = request.remote_addr + + if token.username != username: return False + if token.ua != useragent: return False + return True def is_admin(): - return "admin" in session and session["admin"] - -def add_user(name, username, email, password): - user = Users(name, username, email, password) - db.session.add(user) - db.session.commit() + return is_logged_in() and "admin" in session and session["admin"] def validate_captcha(form): - if "captcha_response" not in form: - return False - captcha_response = form["captcha_response"] - data = {"secret": "6Lc4xhMTAAAAACFaG2NyuKoMdZQtSa_1LI76BCEu", "response": captcha_response} - response = requests.post("https://www.google.com/recaptcha/api/siteverify", data=data) - return response.json()["success"] + if "captcha_response" not in form: + return False + captcha_response = form["captcha_response"] + data = {"secret": "6Lc4xhMTAAAAACFaG2NyuKoMdZQtSa_1LI76BCEu", "response": captcha_response} + response = requests.post("https://www.google.com/recaptcha/api/siteverify", data=data) + return response.json()["success"] \ No newline at end of file diff --git a/server/api/utils.py b/server/api/utils.py index 4b2de57..a8802b6 100644 --- a/server/api/utils.py +++ b/server/api/utils.py @@ -3,6 +3,7 @@ import json import random import string import traceback +import unicodedata from functools import wraps from werkzeug.security import generate_password_hash, check_password_hash @@ -13,8 +14,8 @@ def hash_password(s): def check_password(hashed_password, try_password): return check_password_hash(hashed_password, try_password) -def generate_string(length): - return "".join([random.choice(string.letters + string.digits) for x in range(length)]) +def generate_string(length=32, alpha=string.hexdigits): + return "".join([random.choice(alpha) for x in range(length)]) def unix_time_millis(dt): epoch = datetime.datetime.utcfromtimestamp(0) @@ -22,3 +23,10 @@ def unix_time_millis(dt): def get_time_since_epoch(): return unix_time_millis(datetime.datetime.now()) + +def flat_multi(multidict): + flat = {} + for key, values in multidict.items(): + value = values[0] if type(values) == list and len(values) == 1 else values + flat[key] = unicodedata.normalize("NFKD", value).encode("ascii", "ignore") + return flat \ No newline at end of file diff --git a/server/app.py b/server/app.py index afe1094..6a58dbb 100644 --- a/server/app.py +++ b/server/app.py @@ -6,6 +6,8 @@ import config import json import os +from api.decorators import api_wrapper + app = Flask(__name__) app.config["SQLALCHEMY_DATABASE_URI"] = config.SQLALCHEMY_DATABASE_URI @@ -28,8 +30,9 @@ app.register_blueprint(api.problem.blueprint, url_prefix="/api/problem") api.logger.initialize_logs() @app.route("/api") +@api_wrapper def api_main(): - return json.dumps({ "success": 1, "message": "The API is online." }) + return { "success": 1, "message": "The API is online." } if __name__ == "__main__": with app.app_context(): diff --git a/web/css/easyctf.css b/web/css/easyctf.css new file mode 100644 index 0000000..9325a9e --- /dev/null +++ b/web/css/easyctf.css @@ -0,0 +1,23 @@ +@font-face { + font-family: "Proxima Nova"; + src: url("/fonts/ProximaNova.woff2"); +} +@font-face { + font-family: "Proxima Nova"; + src: url("/fonts/ProximaNovaBold.woff2"); + font-weight: bold; +} + +* { + font-family: "Proxima Nova"; +} + +.tab-content { + padding: 10px; + > .tab-pane { + display: none; + } + > .active { + display: block; + } +} \ No newline at end of file diff --git a/web/fonts/ProximaNova.woff2 b/web/fonts/ProximaNova.woff2 new file mode 100644 index 0000000..a0ccdbc Binary files /dev/null and b/web/fonts/ProximaNova.woff2 differ diff --git a/web/fonts/ProximaNovaBold.woff2 b/web/fonts/ProximaNovaBold.woff2 new file mode 100644 index 0000000..738a92e Binary files /dev/null and b/web/fonts/ProximaNovaBold.woff2 differ diff --git a/web/index.html b/web/index.html index eb5ccf8..ef611de 100644 --- a/web/index.html +++ b/web/index.html @@ -8,15 +8,24 @@ - - + + + + + + + + + + + - -