diff --git a/ctf.nginx b/ctf.nginx index 579eba7..f866686 100644 --- a/ctf.nginx +++ b/ctf.nginx @@ -13,7 +13,7 @@ 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; } 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/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..2d2a9af 100644 --- a/server/api/models.py +++ b/server/api/models.py @@ -1,73 +1,99 @@ from flask.ext.sqlalchemy import SQLAlchemy +import datetime 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)) + utype = 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 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=datetime.datetime.utcnow(), active=True, ua=None, ip=None): + self.sid = utils.generate_string() + self.uid = uid + self.username = username + self.issued = datetime.datetime.utcnow() + 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..c8b4daf 100644 --- a/server/api/user.py +++ b/server/api/user.py @@ -1,96 +1,170 @@ 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 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"]) +@blueprint.route("/logout", methods=["GET", "POST"]) @api_wrapper def user_logout(): - session.clear() + sid = session["sid"] + username = session["username"] + LoginTokens.query.filter_by(sid=sid, username=username).delete() + 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 + +################## +# 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): + with app.app_context(): + 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() }) + 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 + token = LoginTokens(user.uid, user.username, ua=useragent, ip=ip) + with app.app_context(): + db.session.add(token) + db.session.commit() + + session["sid"] = token.sid + session["username"] = token.username + session["admin"] = user.utype == 0 + + 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/web/css/easyctf.css b/web/css/easyctf.css new file mode 100644 index 0000000..4690001 --- /dev/null +++ b/web/css/easyctf.css @@ -0,0 +1,13 @@ +@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"; +} \ 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..47c0305 100644 --- a/web/index.html +++ b/web/index.html @@ -8,15 +8,16 @@ - - + + + - -