diff --git a/ctf.nginx b/ctf.nginx index 155053f..dff29d7 100644 --- a/ctf.nginx +++ b/ctf.nginx @@ -7,6 +7,7 @@ server { index index.html index.htm; server_name localhost; + error_page 404 /404; # location / { # try_files $uri $uri/ =404; @@ -21,7 +22,7 @@ server { default_type text/html; try_files /index.html /index.html; } - location ~^/profile/(.*)$ { + location ~^/(profile|team)/(.*)$ { default_type text/html; try_files /index.html /index.html; } diff --git a/server/api/__init__.py b/server/api/__init__.py index bcc3da4..3edb254 100644 --- a/server/api/__init__.py +++ b/server/api/__init__.py @@ -3,4 +3,5 @@ import logger import models import problem import user +import team import utils diff --git a/server/api/admin.py b/server/api/admin.py index 8af0c3c..cd7983e 100644 --- a/server/api/admin.py +++ b/server/api/admin.py @@ -6,7 +6,7 @@ import json blueprint = Blueprint("admin", __name__) -@blueprint.route("/problems/list", methods=["POST"]) +@blueprint.route("/problems/list", methods=["GET"]) @api_wrapper @admins_only def problem_data(): diff --git a/server/api/decorators.py b/server/api/decorators.py index cea2547..aba964f 100644 --- a/server/api/decorators.py +++ b/server/api/decorators.py @@ -13,9 +13,17 @@ def api_wrapper(f): @wraps(f) def wrapper(*args, **kwds): if request.method == "POST": - token = session.pop("csrf_token") - if not token or token != request.form.get("csrf_token"): - return make_response(json.dumps({ "success": 0, "message": "Token has been tampered with." }), 403, response_header) + try: + token = str(session.pop("csrf_token")) + provided_token = str(request.form.get("csrf_token")) + if not token or token != provided_token: + raise Exception + except Exception, e: + response = make_response(json.dumps({ "success": 0, "message": "Token has been tampered with." }), 403, response_header) + token = utils.generate_string() + response.set_cookie("csrf_token", token) + session["csrf_token"] = token + return response web_result = {} response = 200 @@ -29,11 +37,11 @@ def api_wrapper(f): traceback.print_exc() web_result = { "success": 0, "message": "Something went wrong! Please notify us about this immediately.", "error": [ str(error), traceback.format_exc() ] } result = (json.dumps(web_result), response, response_header) + response = make_response(result) # Setting CSRF token if "token" not in session: token = utils.generate_string() - response = make_response(result) response.set_cookie("csrf_token", token) session["csrf_token"] = token diff --git a/server/api/logger.py b/server/api/logger.py index 5ba55a1..7829286 100644 --- a/server/api/logger.py +++ b/server/api/logger.py @@ -25,7 +25,9 @@ def initialize_logs(): if not os.path.exists(log_path): os.mkdir(log_path) - logs = [os.path.join(log_path, "registrations.log"), os.path.join(log_path, "logins.log"), os.path.join(log_path, "submissions.log")] + # logs = [os.path.join(log_path, "registrations.log"), os.path.join(log_path, "logins.log"), os.path.join(log_path, "submissions.log")] + logs = map(lambda x: os.path.join(log_path, x + ".log"), \ + [ "registrations", "logins", "submissions", "create_team" ]) registration_log = logging.handlers.RotatingFileHandler(logs[0], maxBytes=10000) login_log = logging.handlers.RotatingFileHandler(logs[1], maxBytes=10000) diff --git a/server/api/models.py b/server/api/models.py index 715d880..3dcd683 100644 --- a/server/api/models.py +++ b/server/api/models.py @@ -29,17 +29,23 @@ class Users(db.Model): 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) + teamname = db.Column(db.String(64), unique=True) + teamname_lower = db.Column(db.String(64), 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, teamname, owner): + self.teamname = teamname + self.teamname_lower = teamname.lower() + self.owner = owner + + def get_members(self): + members = [ ] + for member in Users.query.filter_by(tid=self.tid).all(): + members.append({ + "username": member.username + }) + return members class Problems(db.Model): pid = db.Column(db.Integer, primary_key=True) diff --git a/server/api/team.py b/server/api/team.py new file mode 100644 index 0000000..02f7d69 --- /dev/null +++ b/server/api/team.py @@ -0,0 +1,95 @@ +from flask import Blueprint, request +from flask import current_app as app +from voluptuous import Schema, Length, Required + +from models import db, Teams, Users +from decorators import api_wrapper, login_required, WebException +from user import in_team, get_user, is_logged_in +from schemas import verify_to_schema, check + +import utils + +blueprint = Blueprint("team", __name__) + +############### +# TEAM ROUTES # +############### + +@blueprint.route("/create", methods=["POST"]) +@api_wrapper +@login_required +def team_create(): + params = utils.flat_multi(request.form) + user = get_user().first() + if user.tid is not None or user.tid >= 0 or get_team(owner=user.uid).first() is not None: + raise WebException("You're already in a team!") + + verify_to_schema(TeamSchema, params) + teamname = params.get("teamname") + + team = Teams(teamname, user.uid) + with app.app_context(): + db.session.add(team) + db.session.commit() + Users.query.filter_by(uid=user.uid).update({ "tid": team.tid }) + db.session.commit() + + return { "success": 1, "message": "Success!" } + +@blueprint.route("/info", methods=["GET"]) +@api_wrapper +def team_info(): + logged_in = is_logged_in() + me = False + teamname = utils.flat_multi(request.args).get("teamname") + if logged_in: + my_team = get_team().first() + if my_team is not None: + if teamname is None: + teamname = my_team.teamname + me = True + elif teamname.lower() == my_team.teamname.lower(): + me = True + if teamname is None: + raise WebException("No team specified.") + team = get_team(teamname_lower=teamname.lower()).first() + if team is None: + raise WebException("Team not found.") + + teamdata = { + "teamname": team.teamname + } + teamdata["in_team"] = me + return { "success": 1, "team": teamdata } + +################## +# TEAM FUNCTIONS # +################## + +__check_teamname = lambda teamname: get_team(teamname_lower=teamname.lower()).first() is None + +TeamSchema = Schema({ + Required("teamname"): check( + ([str, Length(min=4, max=32)], "Your teamname should be between 4 and 32 characters long."), + ([utils.__check_ascii], "Please only use ASCII characters in your teamname."), + ([__check_teamname], "This teamname is taken, did you forget your password?") + ), +}, extra=True) + +def get_team(tid=None, teamname=None, teamname_lower=None, owner=None): + match = {} + if teamname != None: + match.update({ "teamname": teamname }) + elif teamname_lower != None: + match.update({ "teamname_lower": teamname_lower }) + elif tid != None: + match.update({ "tid": tid }) + elif owner != None: + match.update({ "owner": owner }) + elif is_logged_in(): + user = get_user().first() + if user.tid is not None: + match.update({ "tid": user.tid }) + with app.app_context(): + result = Teams.query.filter_by(**match) + return result \ No newline at end of file diff --git a/server/api/user.py b/server/api/user.py index 730654b..5c2655b 100644 --- a/server/api/user.py +++ b/server/api/user.py @@ -79,6 +79,8 @@ def user_status(): "admin": is_admin(), "username": session["username"] if logged_in else "", } + if logged_in: + result["has_team"] = in_team(get_user().first()) return result @@ -86,7 +88,7 @@ def user_status(): @api_wrapper def user_info(): logged_in = is_logged_in() - username = utils.flat_multi(request.form).get("username") + username = utils.flat_multi(request.args).get("username") if username is None: if logged_in: username = session["username"] @@ -116,8 +118,6 @@ def user_info(): # 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 @@ -125,19 +125,19 @@ 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.") + ([utils.__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."), + ([utils.__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."), + ([utils.__check_ascii], "Please only use ASCII characters in your password."), ), Required("type"): check( ([str, lambda x: x.isdigit()], "Please use the online form.") @@ -155,8 +155,8 @@ def get_user(username=None, username_lower=None, email=None, 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() }) + elif is_logged_in(): + match.update({ "username": session["username"] }) with app.app_context(): result = Users.query.filter_by(**match) return result @@ -184,6 +184,9 @@ def login_user(username, password): return True +def in_team(user): + return user.tid is not None and user.tid >= 0 + def is_logged_in(): if not("sid" in session and "username" in session): return False sid = session["sid"] diff --git a/server/api/utils.py b/server/api/utils.py index a8802b6..8ac6673 100644 --- a/server/api/utils.py +++ b/server/api/utils.py @@ -1,6 +1,7 @@ import datetime import json import random +import re import string import traceback import unicodedata @@ -8,6 +9,9 @@ import unicodedata from functools import wraps from werkzeug.security import generate_password_hash, check_password_hash +__check_email_format = lambda email: re.match(".+@.+\..{2,}", email) is not None +__check_ascii = lambda s: all(c in string.printable for c in s) + def hash_password(s): return generate_password_hash(s) @@ -28,5 +32,5 @@ 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") + flat[key] = value.encode("utf-8") return flat \ No newline at end of file diff --git a/server/app.py b/server/app.py index 6a58dbb..2ec4e61 100644 --- a/server/app.py +++ b/server/app.py @@ -25,8 +25,9 @@ with app.app_context(): app.secret_key = config.SECRET_KEY app.register_blueprint(api.admin.blueprint, url_prefix="/api/admin") -app.register_blueprint(api.user.blueprint, url_prefix="/api/user") app.register_blueprint(api.problem.blueprint, url_prefix="/api/problem") +app.register_blueprint(api.team.blueprint, url_prefix="/api/team") +app.register_blueprint(api.user.blueprint, url_prefix="/api/user") api.logger.initialize_logs() @app.route("/api") diff --git a/web/js/easyctf.js b/web/js/easyctf.js index 659dd0d..65310f8 100644 --- a/web/js/easyctf.js +++ b/web/js/easyctf.js @@ -44,6 +44,14 @@ app.config(function($routeProvider, $locationProvider) { templateUrl: "pages/settings.html", controller: "mainController" }) + .when("/team", { + templateUrl: "pages/team.html", + controller: "teamController" + }) + .when("/team/:teamname", { + templateUrl: "pages/team.html", + controller: "teamController" + }) .when("/admin/problems", { templateUrl: "pages/admin/problems.html", controller: "adminProblemsController" @@ -59,10 +67,9 @@ app.controller("mainController", ["$scope", "$http", function($scope, $http) { $scope.config = { navbar: { } }; $.get("/api/user/status", function(result) { if (result["success"] == 1) { - $scope.config.navbar.logged_in = result["logged_in"]; - $scope.config.navbar.username = result["username"]; - $scope.config.navbar.admin = result["admin"]; - $scope.$emit("adminStatus"); + delete result["success"]; + $scope.config.navbar = result; + $scope.$emit("loginStatus"); } else { $scope.config.navbar.logged_in = false; } @@ -92,9 +99,35 @@ app.controller("profileController", ["$controller", "$scope", "$http", "$routePa }); }]); +app.controller("loginController", ["$controller", "$scope", "$http", function($controller, $scope, $http) { + $controller("mainController", { $scope: $scope }); + $scope.$on("loginStatus", function() { + if ($scope.config["navbar"].logged_in != true) { + location.href = "/login"; + return; + } + }); +}]); + +app.controller("teamController", ["$controller", "$scope", "$http", "$routeParams", function($controller, $scope, $http, $routeParams) { + var data = { }; + if ("teamname" in $routeParams) { + data["teamname"] = $routeParams["teamname"]; + } else { + $controller("loginController", { $scope: $scope }); + } + $.get("/api/team/info", data, function(result) { + if (result["success"] == 1) { + $scope.team = result["team"]; + } + $scope.$apply(); + $(".timeago").timeago(); + }); +}]); + app.controller("adminController", ["$controller", "$scope", "$http", function($controller, $scope, $http) { $controller("mainController", { $scope: $scope }); - $scope.$on("adminStatus", function() { + $scope.$on("loginStatus", function() { if ($scope.config["navbar"].logged_in != true) { location.href = "/login"; return; @@ -176,4 +209,22 @@ var login_form = function() { var result = JSON.parse(jqXHR["responseText"]); display_message("login_msg", "danger", "Error " + jqXHR["status"] + ": " + result["message"]); }); +}; + +// team page + +var create_team = function() { + var input = "#create_team input"; + var data = $("#create_team").serializeObject(); + data["csrf_token"] = $.cookie("csrf_token"); + $.post("/api/team/create", data, function(result) { + if (result["success"] == 1) { + location.reload(true); + } else { + display_message("create_team_msg", "danger", result["message"]); + } + }).fail(function(jqXHR, status, error) { + var result = JSON.parse(jqXHR["responseText"]); + display_message("create_team_msg", "danger", "Error " + jqXHR["status"] + ": " + result["message"]); + }); }; \ No newline at end of file diff --git a/web/pages/learn.html b/web/pages/learn.html new file mode 100644 index 0000000..e69de29 diff --git a/web/pages/profile.html b/web/pages/profile.html index 291b788..e4316c7 100644 --- a/web/pages/profile.html +++ b/web/pages/profile.html @@ -39,7 +39,12 @@

Team Information

- Hi. +
+
+
+

{{ user['me']==true ? "You're" : "This user is" }} not a part of a team.

+ Join or create one now » +
@@ -73,7 +78,7 @@
-
+
diff --git a/web/pages/shell.html b/web/pages/shell.html index 59af329..901619c 100644 --- a/web/pages/shell.html +++ b/web/pages/shell.html @@ -1,11 +1,11 @@
-

Shell

-
-
-

Username: (insert username) | Password: (insert password)

-
-
-

Insert Shell Here

-
-
+

Shell

+
+
+

Username: (insert username) | Password: (insert password)

+
+
+

Insert Shell Here

+
+
diff --git a/web/pages/team.html b/web/pages/team.html new file mode 100644 index 0000000..2b444b3 --- /dev/null +++ b/web/pages/team.html @@ -0,0 +1,71 @@ + + +
+
+
+
+

{{ team['teamname'] }}

+

{{ team['school'] || 'Add School' }}

+
+
+

{{ team['teamname'] }}

+

{{ team['school'] || 'Unknown Affiliation' }}

+
+
+
+
+
+ +

To participate in EasyCTF, you must be on a team. If you'd like to go solo, just create a team by yourself. Read about team eligibility in the rules.

+ +
+
+
+
+
+
+
+ + + + + +
+
+
+
+ + +

You need an invitation to join another team. If you'd like to request to be a member of their team, go to their team page and click the Request button.

+
+ + \ No newline at end of file