Adding email verification

This commit has some model changes. It could be difficult to upgrade to
this commit.
selenium-screenshot-testing
CodeKevin 2016-02-18 02:30:05 -05:00
parent 770d7f5809
commit 4ae11cf7fe
18 changed files with 380 additions and 121 deletions

View File

@ -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

View File

@ -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)}

View File

@ -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/<data>', 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/<data>', 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()

View File

@ -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/<teamid>', 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))

View File

@ -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 = ''

View File

@ -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

View File

@ -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)

View File

@ -25,11 +25,83 @@
<input class="form-control" id='max_tries' name='max_tries' type='text' placeholder="0" {% if max_tries is defined and max_tries != None %}value="{{ max_tries }}"{% endif %}>
</div>
<div class="form-group">
<label for="start">Mailgun API Key:</label>
<input class="form-control" id='mg_api_key' name='mg_api_key' type='text' placeholder="Mailgun API Key" {% if mg_api_key is defined and mg_api_key != None %}value="{{ mg_api_key }}"{% endif %}>
<ul class="nav nav-tabs" role="tablist">
<li role="presentation" class="active">
<a href="#mailserver" aria-controls="mailserver" role="tab" data-toggle="tab">Mail Server</a>
</li>
<li role="presentation">
<a href="#mailgun" aria-controls="mailgun" role="tab" data-toggle="tab">Mailgun</a>
</li>
</ul>
<div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="mailserver">
<div class="form-group">
<label for="start">Mail Server Address:</label>
<input class="form-control" id='mail_server' name='mail_server' type='text'
placeholder="Mail Server Address"
{% if mail_server is defined and mail_server != None %}value="{{ mail_server }}"{% endif %}>
</div>
<div class="form-group">
<label for="start">Mail Server Address:</label>
<input class="form-control" id='mail_server' name='mail_server' type='text'
placeholder="Mail Server Address"
{% if mail_server is defined and mail_server != None %}value="{{ mail_server }}"{% endif %}>
</div>
<div class="form-group">
<label for="start">Mail Server Port:</label>
<input class="form-control" id='mail_port' name='mail_port' type='text'
placeholder="Mail Server Port"
{% if mail_port is defined and mail_port != None %}value="{{ mail_port }}"{% endif %}>
</div>
<div class="form-group">
<label for="start">Username:</label>
<input class="form-control" id='mail_username' name='mail_username' type='text'
placeholder="Username"
{% if mail_username is defined and mail_username != None %}value="{{ mail_username }}"{% endif %}>
</div>
<div class="form-group">
<label for="start">Password:</label>
<input class="form-control" id='mail_password' name='mail_password' type='password'
placeholder="Password"
{% if mail_password is defined and mail_password != None %}value="{{ mail_password }}"{% endif %}>
</div>
<div class="checkbox">
<label>
<input id="mail_ssl" name="mail_ssl" type="checkbox" {% if mail_ssl %}checked{% endif %}>
SSL
</label>
</div>
<div class="checkbox">
<label>
<input id="mail_tls" name="mail_tls" type="checkbox" {% if mail_tls %}checked{% endif %}>
TLS
</label>
</div>
</div>
<div role="tabpanel" class="tab-pane" id="mailgun">
<div class="form-group">
<label for="start">Mailgun API Base URL:</label>
<input class="form-control" id='mg_base_url' name='mg_base_url' type='text'
placeholder="Mailgun API Base URL"
{% if mg_base_url is defined and mg_base_url != None %}value="{{ mg_base_url }}"{% endif %}>
</div>
<div class="form-group">
<label for="start">Mailgun API Key:</label>
<input class="form-control" id='mg_api_key' name='mg_api_key' type='text'
placeholder="Mailgun API Key"
{% if mg_api_key is defined and mg_api_key != None %}value="{{ mg_api_key }}"{% endif %}>
</div>
</div>
</div>
<br>
<div>
<ul class="nav nav-tabs" role="tablist">
<li role="presentation" class="active">
@ -161,6 +233,13 @@
</label>
</div>
<div class="checkbox">
<label>
<input id="verify_emails" name="verify_emails" type="checkbox" {% if verify_emails %}checked{% endif %}>
Only allow users with verified emails
</label>
</div>
<div class="checkbox">
<label>
<input id="view_challenges_unregistered" name="view_challenges_unregistered" type="checkbox" {% if view_challenges_unregistered %}checked{% endif %}>

View File

@ -45,9 +45,10 @@ input[type="checkbox"] { margin: 0px !important; position: relative; top: 5px; }
<form method="POST">
<input type="hidden" name="id">
<input type="hidden" name="nonce" value="{{ nonce }}">
<textarea name="msg" placeholder="Enter your message here" rows="15"></textarea>
<textarea class="form-control" name="msg" placeholder="Enter your message here" rows="15"></textarea>
<br>
<div id="email-user-errors"></div>
<button type="button" id="send-user-email">Send Message</button>
<button type="button" id="send-user-email" class="btn btn-theme btn-outlined">Send Message</button>
</form>
</div>
</div>

View File

@ -0,0 +1,51 @@
{% extends "base.html" %}
{% block stylesheets %}
<link rel="stylesheet" href="/static/css/input.css">
<style>
#login-container {
padding-left: 60px;
padding-right: 60px;
}
.done-row {
padding-top: 15px;
margin: 0px;
}
</style>
{% endblock %}
{% block content %}
<div class="jumbotron home">
<div class="container">
<h1>Confirm</h1>
</div>
</div>
<div class="container main-container">
<div class="row">
<div id="login-container" class="col-md-6 col-md-offset-3">
{% for error in errors %}
<div class="alert alert-danger alert-dismissable" role="alert">
<span class="sr-only">Error:</span>
{{ error }}
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span
aria-hidden="true">×</span></button>
</div>
{% endfor %}
<h3 class="text-center">
We've sent a confirmation email to {{ team.email }}
</h3>
<h3 class="text-center">
Please click the link that email to confirm your account and access the rest of the CTF.
</h3>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="//cdnjs.cloudflare.com/ajax/libs/classie/1.0.1/classie.min.js"></script>
<script src="/static/js/input.js"></script>
{% endblock %}

View File

@ -1 +0,0 @@
CTF

View File

@ -40,6 +40,14 @@
</div>
{% endfor %}
{% endif %}
{% if confirm_email %}
<div class="alert alert-info alert-dismissable submit-row" role="alert">
Your email address isn't confirmed!
Please check your email to confirm your email address.
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span
aria-hidden="true">×</span></button>
</div>
{% endif %}
<form method="post" accept-charset="utf-8" autocomplete="off" role="form" class="form-horizontal">
<div class="row">
<div class="col-md-12">

View File

@ -14,13 +14,14 @@
<div class="row">
<div id="login-container" class="col-md-6 col-md-offset-3">
{% for error in errors %}
<div class="alert alert-danger alert-dismissable" role="alert">
<div class="alert alert-info alert-dismissable" role="alert">
<span class="sr-only">Error:</span>
{{ error }}
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">×</span></button>
</div>
{% endfor %}
<form method="post" accept-charset="utf-8" autocomplete="off" role="form" class="form-horizontal">
<input name='nonce' type='hidden' value="{{ nonce }}">
{% if mode %}
<div class="row">
<div class="col-md-12">

View File

@ -32,7 +32,7 @@
<form method="post" accept-charset="utf-8" autocomplete="off" role="form" class="form-horizontal">
<input type="hidden" name="nonce" value="{{nonce}}"> {# This nonce is implemented specially in the route itself #}
<span class="input">
<input class="input-field" type="text" name="ctf_name" id="ctf_name-input" />
<input class="input-field" type="text" name="ctf_name" id="ctf_name-input"/>
<label class="input-label" for="ctf_name-input">
<span class="label-content" data-content="CTF Name">CTF Name</span>
</label>
@ -40,39 +40,24 @@
<span class="input">
<input class="input-field" type="text" name="name" id="username-input" />
<label class="input-label" for="username-input">
<span class="label-content" data-content="Username">Username</span>
<span class="label-content" data-content="Username">Admin Username</span>
</label>
</span>
<span class="input">
<input class="input-field" type="email" name="email" id="email-input" />
<label class="input-label" for="email-input">
<span class="label-content" data-content="Email">Email</span>
<span class="label-content" data-content="Email">Admin Email</span>
</label>
</span>
<span class="input">
<input class="input-field" type="password" name="password" id="password-input" />
<label class="input-label" for="password-input">
<span class="label-content" data-content="Password">Password</span>
<span class="label-content" data-content="Password">Admin Password</span>
</label>
</span>
<br/>
</div>
</div>
<div class="container" style="margin-bottom:50px;">
<h2 class="text-center">Index Page</h2>
<textarea id="pages-editor" name="html">
<div class="container main-container">
<img class="logo" src="/static/img/logo.png" />
<h3 class="text-center">
Welcome to a cool CTF framework 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>
</div>
<div class="container main-container" style="margin-bottom:100px;">
<div id="login-container" class="col-md-6 col-md-offset-3">
<div class="submit-row text-center">

View File

@ -1,10 +1,10 @@
from CTFd.models import db, WrongKeys, Pages, Config, Tracking
from CTFd import mail
from CTFd.models import db, WrongKeys, Pages, Config, Tracking, Teams
from six.moves.urllib.parse import urlparse, urljoin
from functools import wraps
from flask import current_app as app, g, request, redirect, url_for, session, render_template, abort
from flask.ext.mail import Message
from itsdangerous import Signer, BadSignature
from socket import inet_aton, inet_ntoa
from struct import unpack, pack
from sqlalchemy.engine.url import make_url
@ -20,7 +20,8 @@ import os
import sys
import re
import time
import smtplib
import email
def init_logs(app):
logger_keys = logging.getLogger('keys')
@ -138,6 +139,12 @@ def pages():
def authed():
return bool(session.get('id', False))
def is_verified():
team = Teams.query.filter_by(id=session.get('id')).first()
if team:
return team.verified
else:
return False
def is_setup():
setup = Config.query.filter_by(key='setup').first()
@ -261,14 +268,19 @@ def ip2long(ip):
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())
return len(db.session.query(WrongKeys).filter(WrongKeys.teamid == teamid, WrongKeys.date >= one_min_ago).all())
def get_config(key):
config = Config.query.filter_by(key=key).first()
if config:
return config.value
value = config.value
if value and value.isdigit():
return int(value)
else:
return value
else:
set_config(key, None)
return None
@ -284,34 +296,75 @@ def set_config(key, value):
def mailserver():
if (get_config('mg_api_key') and app.config['ADMINS']) or (app.config['MAIL_SERVER'] and app.config['MAIL_PORT'] and app.config['ADMINS']):
if (get_config('mg_api_key')) or (get_config('mail_server') and get_config('mail_port')):
return True
return False
def get_smtp(host, port, username=None, password=None, TLS=None, SSL=None):
smtp = smtplib.SMTP(host, port)
smtp.ehlo()
if TLS:
smtp.starttls()
smtp.ehlo()
smtp.login(username, password)
return smtp
def sendmail(addr, text):
if get_config('mg_api_key') and app.config['ADMINS']:
if get_config('mg_api_key') and get_config('mg_base_url'):
ctf_name = get_config('ctf_name')
mg_api_key = get_config('mg_api_key')
return requests.post(
"https://api.mailgun.net/v2/mail"+app.config['HOST']+"/messages",
mg_base_url = get_config('mg_base_url')
r = requests.post(
mg_base_url + '/messages',
auth=("api", mg_api_key),
data={"from": "{} Admin <{}>".format(ctf_name, app.config['ADMINS'][0]),
data={"from": "{} Admin <{}>".format(ctf_name, 'noreply@ctfd.io'),
"to": [addr],
"subject": "Message from {0}".format(ctf_name),
"text": text})
elif app.config['MAIL_SERVER'] and app.config['MAIL_PORT'] and app.config['ADMINS']:
try:
msg = Message("Message from {0}".format(get_config('ctf_name')), sender=app.config['ADMINS'][0], recipients=[addr])
msg.body = text
mail.send(msg)
if r.status_code == 200:
return True
except:
else:
return False
elif get_config('mail_server') and get_config('mail_port'):
data = {
'host': get_config('mail_server'),
'port': int(get_config('mail_port'))
}
if get_config('mail_username'):
data['username'] = get_config('mail_username')
if get_config('mail_password'):
data['password'] = get_config('mail_password')
if get_config('mail_tls'):
data['TLS'] = get_config('mail_tls')
if get_config('mail_ssl'):
data['SSL'] = get_config('mail_ssl')
smtp = get_smtp(**data)
msg = email.mime.text.MIMEText(text)
msg['Subject'] = "Message from {0}".format(get_config('ctf_name'))
msg['From'] = 'noreply@ctfd.io'
msg['To'] = addr
smtp.sendmail(msg['From'], [msg['To']], msg.as_string())
smtp.quit()
return True
else:
return False
def verify_email(addr):
s = Signer(app.config['SECRET_KEY'])
token = s.sign(addr)
text = """Please click the following link to confirm your email address for {}: {}""".format(
get_config('ctf_name'),
url_for('auth.confirm_user', _external=True) + '/' + token.encode('base64')
)
sendmail(addr, text)
def rmdir(dir):
shutil.rmtree(dir, ignore_errors=True)

View File

@ -49,8 +49,16 @@ def setup():
admin.banned = True
## Index page
html = request.form['html']
page = Pages('index', html)
page = Pages('index', """<div class="container main-container">
<img class="logo" src="/static/img/logo.png" />
<h3 class="text-center">
Welcome to a cool CTF framework 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>""")
#max attempts per challenge
max_tries = Config("max_tries",0)
@ -66,6 +74,22 @@ def setup():
## Allow/Disallow registration
prevent_registration = Config('prevent_registration', None)
## Verify emails
verify_emails = Config('verify_emails', None)
mail_server = Config('mail_server', None)
mail_port = Config('mail_port', None)
mail_tls = Config('mail_tls', None)
mail_ssl = Config('mail_ssl', None)
mail_username = Config('mail_username', None)
mail_password = Config('mail_password', None)
db.session.add(mail_server)
db.session.add(mail_port)
db.session.add(mail_tls)
db.session.add(mail_ssl)
db.session.add(mail_username)
db.session.add(mail_password)
setup = Config('setup', True)
db.session.add(ctf_name)
@ -76,6 +100,7 @@ def setup():
db.session.add(end)
db.session.add(view_challenges_unregistered)
db.session.add(prevent_registration)
db.session.add(verify_emails)
db.session.add(css)
db.session.add(setup)
db.session.commit()
@ -113,8 +138,8 @@ def teams(page):
page_start = results_per_page * ( page - 1 )
page_end = results_per_page * ( page - 1 ) + results_per_page
teams = Teams.query.slice(page_start, page_end).all()
count = db.session.query(db.func.count(Teams.id)).first()[0]
teams = Teams.query.filter_by(verified=True).slice(page_start, page_end).all()
count = len(teams)
pages = int(count / results_per_page) + (count % results_per_page > 0)
return render_template('teams.html', teams=teams, team_pages=pages, curr_page=page)
@ -178,7 +203,10 @@ def profile():
team = Teams.query.filter_by(id=session['id']).first()
if not get_config('prevent_name_change'):
team.name = name
team.email = email
if team.email != email.lower():
team.email = email.lower()
if get_config('verify_emails'):
team.verified = False
session['username'] = team.name
if 'password' in request.form.keys() and not len(request.form['password']) == 0:
@ -197,7 +225,8 @@ def profile():
affiliation = user.affiliation
country = user.country
prevent_name_change = get_config('prevent_name_change')
confirm_email = not user.verified
return render_template('profile.html', name=name, email=email, website=website, affiliation=affiliation,
country=country, prevent_name_change=prevent_name_change)
country=country, prevent_name_change=prevent_name_change, confirm_email=confirm_email)
else:
return redirect(url_for('auth.login'))

View File

@ -246,7 +246,9 @@ if __name__ == '__main__':
name = gen_name()
if name not in used:
used.append(name)
db.session.add(Teams(name, name.lower() + gen_email(), 'password'))
team = Teams(name, name.lower() + gen_email(), 'password')
team.verified = True
db.session.add(team)
count += 1
db.session.commit()

View File

@ -1,6 +1,4 @@
APScheduler
Flask
Flask-Mail
Flask-SQLAlchemy
Flask-Session
SQLAlchemy