From 4ae11cf7fea858e63d58111a33875f54fed37276 Mon Sep 17 00:00:00 2001 From: CodeKevin Date: Thu, 18 Feb 2016 02:30:05 -0500 Subject: [PATCH] Adding email verification This commit has some model changes. It could be difficult to upgrade to this commit. --- CTFd/__init__.py | 4 -- CTFd/admin.py | 90 ++++++++++++++++++------------ CTFd/auth.py | 41 ++++++++++++-- CTFd/challenges.py | 10 ++-- CTFd/config.py | 14 ----- CTFd/models.py | 7 ++- CTFd/scoreboard.py | 27 +++++++-- CTFd/templates/admin/config.html | 85 +++++++++++++++++++++++++++- CTFd/templates/admin/teams.html | 5 +- CTFd/templates/confirm.html | 51 +++++++++++++++++ CTFd/templates/ctf.html | 1 - CTFd/templates/profile.html | 8 +++ CTFd/templates/reset_password.html | 3 +- CTFd/templates/setup.html | 23 ++------ CTFd/utils.py | 85 ++++++++++++++++++++++------ CTFd/views.py | 41 ++++++++++++-- populate.py | 4 +- requirements.txt | 2 - 18 files changed, 380 insertions(+), 121 deletions(-) create mode 100644 CTFd/templates/confirm.html delete mode 100644 CTFd/templates/ctf.html diff --git a/CTFd/__init__.py b/CTFd/__init__.py index 4b91659d..b30af101 100644 --- a/CTFd/__init__.py +++ b/CTFd/__init__.py @@ -1,6 +1,5 @@ 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 os @@ -19,9 +18,6 @@ def create_app(config='CTFd.config'): app.db = db - global mail - mail = Mail(app) - #Session(app) from CTFd.views import views diff --git a/CTFd/admin.py b/CTFd/admin.py index d5b6309c..4a5027c9 100644 --- a/CTFd/admin.py +++ b/CTFd/admin.py @@ -69,21 +69,38 @@ def admin_config(): prevent_registration = bool(request.form.get('prevent_registration', None)) prevent_name_change = bool(request.form.get('prevent_name_change', None)) view_after_ctf = bool(request.form.get('view_after_ctf', None)) + verify_emails = bool(request.form.get('verify_emails', None)) + mail_tls = bool(request.form.get('mail_tls', None)) + mail_ssl = bool(request.form.get('mail_ssl', None)) except (ValueError, TypeError): view_challenges_unregistered = None prevent_registration = None prevent_name_change = None view_after_ctf = None + verify_emails = None + mail_tls = None + mail_ssl = None finally: view_challenges_unregistered = set_config('view_challenges_unregistered', view_challenges_unregistered) prevent_registration = set_config('prevent_registration', prevent_registration) prevent_name_change = set_config('prevent_name_change', prevent_name_change) view_after_ctf = set_config('view_after_ctf', view_after_ctf) + verify_emails = set_config('verify_emails', verify_emails) + mail_tls = set_config('mail_tls', mail_tls) + mail_ssl = set_config('mail_ssl', mail_ssl) + + mail_server = set_config("mail_server", request.form.get('mail_server', None)) + mail_port = set_config("mail_port", request.form.get('mail_port', None)) + + mail_username = set_config("mail_username", request.form.get('mail_username', None)) + mail_password = set_config("mail_password", request.form.get('mail_password', None)) ctf_name = set_config("ctf_name", request.form.get('ctf_name', None)) - mg_api_key = set_config("mg_api_key", request.form.get('mg_api_key', None)) - max_tries = set_config("max_tries", request.form.get('max_tries', None)) + mg_base_url = set_config("mg_base_url", request.form.get('mg_base_url', None)) + mg_api_key = set_config("mg_api_key", request.form.get('mg_api_key', None)) + + max_tries = set_config("max_tries", request.form.get('max_tries', None)) db_start = Config.query.filter_by(key='start').first() db_start.value = start @@ -98,42 +115,30 @@ def admin_config(): return redirect(url_for('admin.admin_config')) ctf_name = get_config('ctf_name') - if not ctf_name: - set_config('ctf_name', None) + + mail_server = get_config('mail_server') + mail_port = get_config('mail_port') + mail_username = get_config('mail_username') + mail_password = get_config('mail_password') mg_api_key = get_config('mg_api_key') - if not mg_api_key: - set_config('mg_api_key', None) - + mg_base_url = get_config('mg_base_url') max_tries = get_config('max_tries') if not max_tries: set_config('max_tries', 0) max_tries = 0 - view_after_ctf = get_config('view_after_ctf') == '1' - if not view_after_ctf: - set_config('view_after_ctf', 0) - view_after_ctf = 0 - + view_after_ctf = get_config('view_after_ctf') start = get_config('start') - if not start: - set_config('start', None) - end = get_config('end') - if not end: - set_config('end', None) - view_challenges_unregistered = get_config('view_challenges_unregistered') == '1' - if not view_challenges_unregistered: - set_config('view_challenges_unregistered', None) + mail_tls = get_config('mail_tls') + mail_ssl = get_config('mail_ssl') - prevent_registration = get_config('prevent_registration') == '1' - if not prevent_registration: - set_config('prevent_registration', None) - - prevent_name_change = get_config('prevent_name_change') == '1' - if not prevent_name_change: - set_config('prevent_name_change', None) + view_challenges_unregistered = get_config('view_challenges_unregistered') + prevent_registration = get_config('prevent_registration') + prevent_name_change = get_config('prevent_name_change') + verify_emails = get_config('verify_emails') db.session.commit() db.session.close() @@ -155,12 +160,27 @@ def admin_config(): end = datetime.datetime.fromtimestamp(float(end)) end_days = calendar.monthrange(end.year, end.month)[1] - return render_template('admin/config.html', ctf_name=ctf_name, start=start, end=end, + return render_template('admin/config.html', + ctf_name=ctf_name, + start=start, + end=end, max_tries=max_tries, + mail_server=mail_server, + mail_port=mail_port, + mail_username=mail_username, + mail_password=mail_password, + mail_tls=mail_tls, + mail_ssl=mail_ssl, view_challenges_unregistered=view_challenges_unregistered, - prevent_registration=prevent_registration, mg_api_key=mg_api_key, + prevent_registration=prevent_registration, + mg_base_url=mg_base_url, + mg_api_key=mg_api_key, prevent_name_change=prevent_name_change, - view_after_ctf=view_after_ctf, months=months, curr_year=curr_year, start_days=start_days, + verify_emails=verify_emails, + view_after_ctf=view_after_ctf, + months=months, + curr_year=curr_year, + start_days=start_days, end_days=end_days) @@ -371,7 +391,7 @@ def admin_team(teamid): solve_ids = [s.chalid for s in solves] missing = Challenges.query.filter( not_(Challenges.id.in_(solve_ids) ) ).all() addrs = Tracking.query.filter_by(team=teamid).order_by(Tracking.date.desc()).group_by(Tracking.ip).all() - wrong_keys = WrongKeys.query.filter_by(team=teamid).order_by(WrongKeys.date.desc()).all() + wrong_keys = WrongKeys.query.filter_by(teamid=teamid).order_by(WrongKeys.date.desc()).all() score = user.score() place = user.place() return render_template('admin/team.html', solves=solves, team=user, addrs=addrs, score=score, missing=missing, @@ -451,7 +471,7 @@ def unban(teamid): @admins_only def delete_team(teamid): try: - WrongKeys.query.filter_by(team=teamid).delete() + WrongKeys.query.filter_by(teamid=teamid).delete() Solves.query.filter_by(teamid=teamid).delete() Tracking.query.filter_by(team=teamid).delete() Teams.query.filter_by(id=teamid).delete() @@ -565,7 +585,7 @@ def admin_wrong_key(page='1'): page_start = results_per_page * ( page - 1 ) page_end = results_per_page * ( page - 1 ) + results_per_page - wrong_keys = WrongKeys.query.add_columns(WrongKeys.chalid, WrongKeys.flag, WrongKeys.team, WrongKeys.date,\ + wrong_keys = WrongKeys.query.add_columns(WrongKeys.chalid, WrongKeys.flag, WrongKeys.teamid, WrongKeys.date,\ Challenges.name.label('chal_name'), Teams.name.label('team_name')).\ join(Challenges).join(Teams).order_by('team_name ASC').slice(page_start, page_end).all() @@ -597,13 +617,13 @@ def admin_correct_key(page='1'): @admins_only def admin_fails(teamid='all'): if teamid == "all": - fails = WrongKeys.query.join(Teams, WrongKeys.team == Teams.id).filter(Teams.banned==None).count() + fails = WrongKeys.query.join(Teams, WrongKeys.teamid == Teams.id).filter(Teams.banned==None).count() solves = Solves.query.join(Teams, Solves.teamid == Teams.id).filter(Teams.banned==None).count() db.session.close() json_data = {'fails':str(fails), 'solves': str(solves)} return jsonify(json_data) else: - fails = WrongKeys.query.filter_by(team=teamid).count() + fails = WrongKeys.query.filter_by(teamid=teamid).count() solves = Solves.query.filter_by(teamid=teamid).count() db.session.close() json_data = {'fails':str(fails), 'solves': str(solves)} diff --git a/CTFd/auth.py b/CTFd/auth.py index 457c1814..9609aff9 100644 --- a/CTFd/auth.py +++ b/CTFd/auth.py @@ -1,8 +1,8 @@ from flask import render_template, request, redirect, abort, jsonify, url_for, session, Blueprint -from CTFd.utils import sha512, is_safe_url, authed, mailserver, sendmail, can_register +from CTFd.utils import sha512, is_safe_url, authed, mailserver, sendmail, can_register, get_config, verify_email from CTFd.models import db, Teams -from itsdangerous import TimedSerializer, BadTimeSignature +from itsdangerous import TimedSerializer, BadTimeSignature, Signer, BadSignature from passlib.hash import bcrypt_sha256 from flask import current_app as app @@ -14,6 +14,32 @@ import os auth = Blueprint('auth', __name__) +@auth.route('/confirm', methods=['POST', 'GET']) +@auth.route('/confirm/', methods=['GET']) +def confirm_user(data=None): + if not get_config('verify_emails'): + return redirect(url_for('challenges.challenges_view')) + if data and request.method == "GET": ## User is confirming email account + try: + s = Signer(app.config['SECRET_KEY']) + email = s.unsign(data.decode('base64')) + except BadSignature: + return render_template('confirm.html', errors=['Your confirmation link seems wrong']) + team = Teams.query.filter_by(email=email).first() + team.verified = True + db.session.commit() + db.session.close() + if authed(): + return redirect(url_for('challenges.challenges_view')) + return redirect(url_for('auth.login')) + if not data and request.method == "GET": ## User has been directed to the confirm page because his account is not verified + team = Teams.query.filter_by(id=session['id']).first() + if team.verified: + return redirect(url_for('views.profile')) + return render_template('confirm.html', team=team) + + + @auth.route('/reset_password', methods=['POST', 'GET']) @auth.route('/reset_password/', methods=['POST', 'GET']) def reset_password(data=None): @@ -43,7 +69,7 @@ Did you initiate a password reset? {0}/reset_password/{1} -""".format(app.config['HOST'], token.encode('base64')) +""".format(url_for('auth.reset_password', _external=True), token.encode('base64')) sendmail(email, text) @@ -85,7 +111,7 @@ def register(): return render_template('register.html', errors=errors, name=request.form['name'], email=request.form['email'], password=request.form['password']) else: with app.app_context(): - team = Teams(name, email, password) + team = Teams(name, email.lower(), password) db.session.add(team) db.session.commit() db.session.flush() @@ -95,8 +121,11 @@ def register(): session['admin'] = team.admin session['nonce'] = sha512(os.urandom(10)) - if mailserver(): - sendmail(request.form['email'], "You've successfully registered for the CTF") + if mailserver() and get_config('verify_emails'): + verify_email(team.email) + else: + if mailserver(): + sendmail(request.form['email'], "You've successfully registered for {}".format(get_config('ctf_name'))) db.session.close() diff --git a/CTFd/challenges.py b/CTFd/challenges.py index 2ebd725e..61935d70 100644 --- a/CTFd/challenges.py +++ b/CTFd/challenges.py @@ -1,6 +1,6 @@ from flask import current_app as app, render_template, request, redirect, abort, jsonify, json as json_mod, url_for, session, Blueprint -from CTFd.utils import ctftime, view_after_ctf, authed, unix_time, get_kpm, can_view_challenges, is_admin, get_config, get_ip +from CTFd.utils import ctftime, view_after_ctf, authed, unix_time, get_kpm, can_view_challenges, is_admin, get_config, get_ip, is_verified from CTFd.models import db, Challenges, Files, Solves, WrongKeys, Keys, Tags, Teams import time @@ -19,6 +19,8 @@ def challenges_view(): pass else: return redirect('/') + if get_config('verify_emails') and not is_verified(): + return redirect(url_for('auth.confirm_user')) if can_view_challenges(): return render_template('chals.html', ctftime=ctftime()) else: @@ -84,7 +86,7 @@ def attempts(): chals = Challenges.query.add_columns('id').all() json = {'maxattempts':[]} for chal, chalid in chals: - fails = WrongKeys.query.filter_by(team=session['id'], chalid=chalid).count() + fails = WrongKeys.query.filter_by(teamid=session['id'], chalid=chalid).count() if fails >= int(get_config("max_tries")) and int(get_config("max_tries")) > 0: json['maxattempts'].append({'chalid':chalid}) return jsonify(json) @@ -92,7 +94,7 @@ def attempts(): @challenges.route('/fails/', methods=['GET']) def fails(teamid): - fails = WrongKeys.query.filter_by(team=teamid).count() + fails = WrongKeys.query.filter_by(teamid=teamid).count() solves = Solves.query.filter_by(teamid=teamid).count() db.session.close() json = {'fails':str(fails), 'solves': str(solves)} @@ -113,7 +115,7 @@ def chal(chalid): if not ctftime(): return redirect(url_for('challenges.challenges_view')) if authed(): - fails = WrongKeys.query.filter_by(team=session['id'], chalid=chalid).count() + fails = WrongKeys.query.filter_by(teamid=session['id'], chalid=chalid).count() logger = logging.getLogger('keys') data = (time.strftime("%m/%d/%Y %X"), session['username'].encode('utf-8'), request.form['key'].encode('utf-8'), get_kpm(session['id'])) print("[{0}] {1} submitted {2} with kpm {3}".format(*data)) diff --git a/CTFd/config.py b/CTFd/config.py index f2c5e041..cf55c7fd 100644 --- a/CTFd/config.py +++ b/CTFd/config.py @@ -28,17 +28,3 @@ TRUSTED_PROXIES = [ '^172\.(1[6-9]|2[0-9]|3[0-1])\.', '^192\.168\.' ] - -##### EMAIL (Mailgun and non-Mailgun) ##### - -# The first address will be used as the from address of messages sent from CTFd -ADMINS = [] - -##### EMAIL (if not using Mailgun) ##### -CTF_NAME = '' -MAIL_SERVER = '' -MAIL_PORT = 0 -MAIL_USE_TLS = False -MAIL_USE_SSL = False -MAIL_USERNAME = '' -MAIL_PASSWORD = '' diff --git a/CTFd/models.py b/CTFd/models.py index 92599188..1da1af64 100644 --- a/CTFd/models.py +++ b/CTFd/models.py @@ -108,6 +108,7 @@ class Teams(db.Model): country = db.Column(db.String(32)) bracket = db.Column(db.String(32)) banned = db.Column(db.Boolean) + verified = db.Column(db.Boolean) admin = db.Column(db.Boolean) def __init__(self, name, email, password): @@ -165,13 +166,13 @@ class Solves(db.Model): class WrongKeys(db.Model): id = db.Column(db.Integer, primary_key=True) chalid = db.Column(db.Integer, db.ForeignKey('challenges.id')) - team = db.Column(db.Integer, db.ForeignKey('teams.id')) + teamid = db.Column(db.Integer, db.ForeignKey('teams.id')) date = db.Column(db.DateTime, default=datetime.datetime.utcnow) flag = db.Column(db.Text) chal = db.relationship('Challenges', foreign_keys="WrongKeys.chalid", lazy='joined') - def __init__(self, team, chalid, flag): - self.team = team + def __init__(self, teamid, chalid, flag): + self.teamid = teamid self.chalid = chalid self.flag = flag diff --git a/CTFd/scoreboard.py b/CTFd/scoreboard.py index c46ac7f7..6c078347 100644 --- a/CTFd/scoreboard.py +++ b/CTFd/scoreboard.py @@ -9,7 +9,11 @@ scoreboard = Blueprint('scoreboard', __name__) def scoreboard_view(): 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.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('scoreboard.html', teams=teams) @@ -18,7 +22,11 @@ def scoreboard_view(): def 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) + 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 = {'standings':[]} for i, x in enumerate(teams): @@ -39,12 +47,23 @@ def topteams(count): 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).limit(count) + 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)\ + .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)}) + 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) diff --git a/CTFd/templates/admin/config.html b/CTFd/templates/admin/config.html index db966ee2..ad4cbb07 100644 --- a/CTFd/templates/admin/config.html +++ b/CTFd/templates/admin/config.html @@ -25,11 +25,83 @@ -
- - + + + +
+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ + +
+
+ + +
+
+ +
+
+ +
+
+
+
+ + +
+
+ + +
+
+ +
+
+
+ +
+
diff --git a/CTFd/templates/confirm.html b/CTFd/templates/confirm.html new file mode 100644 index 00000000..cb854f69 --- /dev/null +++ b/CTFd/templates/confirm.html @@ -0,0 +1,51 @@ +{% extends "base.html" %} + +{% block stylesheets %} + + +{% endblock %} + +{% block content %} +
+
+

Confirm

+
+
+
+
+
+ {% for error in errors %} + + {% endfor %} +

+ We've sent a confirmation email to {{ team.email }} +

+ +

+ Please click the link that email to confirm your account and access the rest of the CTF. +

+
+
+
+{% endblock %} + +{% block scripts %} + + +{% endblock %} + diff --git a/CTFd/templates/ctf.html b/CTFd/templates/ctf.html deleted file mode 100644 index 48c6db4b..00000000 --- a/CTFd/templates/ctf.html +++ /dev/null @@ -1 +0,0 @@ -CTF \ No newline at end of file diff --git a/CTFd/templates/profile.html b/CTFd/templates/profile.html index f4e74f94..623ebeda 100644 --- a/CTFd/templates/profile.html +++ b/CTFd/templates/profile.html @@ -40,6 +40,14 @@ {% endfor %} {% endif %} + {% if confirm_email %} + + {% endif %}
diff --git a/CTFd/templates/reset_password.html b/CTFd/templates/reset_password.html index 3d91078d..6cc3c03d 100644 --- a/CTFd/templates/reset_password.html +++ b/CTFd/templates/reset_password.html @@ -14,13 +14,14 @@
{% for error in errors %} -