diff --git a/CTFd/__init__.py b/CTFd/__init__.py index fcfd4aa..48b1254 100644 --- a/CTFd/__init__.py +++ b/CTFd/__init__.py @@ -4,9 +4,11 @@ from logging.handlers import RotatingFileHandler from flask_session import Session from sqlalchemy_utils import database_exists, create_database from jinja2 import FileSystemLoader, TemplateNotFound -from utils import get_config, set_config +from utils import get_config, set_config, cache import os import sqlalchemy +from sqlalchemy.engine.url import make_url +from sqlalchemy.exc import OperationalError class ThemeLoader(FileSystemLoader): @@ -24,15 +26,26 @@ def create_app(config='CTFd.config'): from CTFd.models import db, Teams, Solves, Challenges, WrongKeys, Keys, Tags, Files, Tracking - ## sqlite database creation is relative to the script which causes issues with serve.py - if not database_exists(app.config['SQLALCHEMY_DATABASE_URI']) and not app.config['SQLALCHEMY_DATABASE_URI'].startswith('sqlite'): - create_database(app.config['SQLALCHEMY_DATABASE_URI']) + url = make_url(app.config['SQLALCHEMY_DATABASE_URI']) + if url.drivername == 'postgres': + url.drivername = 'postgresql' db.init_app(app) - db.create_all() + + try: + if not database_exists(url): + create_database(url) + db.create_all() + except OperationalError: + db.create_all() + else: + db.create_all() app.db = db + cache.init_app(app) + app.cache = cache + if not get_config('ctf_theme'): set_config('ctf_theme', 'original') diff --git a/CTFd/admin.py b/CTFd/admin.py index 53ebdf5..cf77d79 100644 --- a/CTFd/admin.py +++ b/CTFd/admin.py @@ -1,7 +1,7 @@ from flask import render_template, request, redirect, abort, jsonify, url_for, session, Blueprint from CTFd.utils import sha512, is_safe_url, authed, admins_only, is_admin, unix_time, unix_time_millis, get_config, \ set_config, sendmail, rmdir, create_image, delete_image, run_image, container_status, container_ports, \ - container_stop, container_start, get_themes + container_stop, container_start, get_themes, cache from CTFd.models import db, Teams, Solves, Awards, Containers, Challenges, WrongKeys, Keys, Tags, Files, Tracking, Pages, Config, DatabaseError from CTFd.scoreboard import get_standings from itsdangerous import TimedSerializer, BadTimeSignature @@ -105,8 +105,12 @@ def admin_config(): db.session.commit() db.session.close() + with app.app_context(): + cache.clear() return redirect(url_for('admin.admin_config')) + with app.app_context(): + cache.clear() ctf_name = get_config('ctf_name') ctf_theme = get_config('ctf_theme') max_tries = get_config('max_tries') @@ -758,7 +762,7 @@ def admin_wrong_key(page='1'): wrong_keys = WrongKeys.query.add_columns(WrongKeys.id, 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() + join(Challenges).join(Teams).order_by(WrongKeys.date.desc()).slice(page_start, page_end).all() wrong_count = db.session.query(db.func.count(WrongKeys.id)).first()[0] pages = int(wrong_count / results_per_page) + (wrong_count % results_per_page > 0) @@ -776,7 +780,7 @@ def admin_correct_key(page='1'): solves = Solves.query.add_columns(Solves.id, Solves.chalid, Solves.teamid, Solves.date, Solves.flag, \ 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() + join(Challenges).join(Teams).order_by(Solves.date.desc()).slice(page_start, page_end).all() solve_count = db.session.query(db.func.count(Solves.id)).first()[0] pages = int(solve_count / results_per_page) + (solve_count % results_per_page > 0) diff --git a/CTFd/auth.py b/CTFd/auth.py index 9f265e4..bbc14db 100644 --- a/CTFd/auth.py +++ b/CTFd/auth.py @@ -10,6 +10,7 @@ import logging import time import re import os +import urllib auth = Blueprint('auth', __name__) @@ -22,20 +23,28 @@ def confirm_user(data=None): if data and request.method == "GET": ## User is confirming email account try: s = Signer(app.config['SECRET_KEY']) - email = s.unsign(data.decode('base64')) + email = s.unsign(urllib.unquote(data.decode('base64'))) except BadSignature: return render_template('confirm.html', errors=['Your confirmation link seems wrong']) + except: + return render_template('reset_password.html', errors=['Your link appears broken, please try again.']) team = Teams.query.filter_by(email=email).first() team.verified = True db.session.commit() db.session.close() + logger = logging.getLogger('regs') + logger.warn("[{0}] {1} confirmed {2}".format(time.strftime("%m/%d/%Y %X"), team.name.encode('utf-8'), team.email.encode('utf-8'))) 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 + if not data and request.method == "GET": ## User has been directed to the confirm page because his account is not verified + if not authed(): + return redirect(url_for('auth.login')) team = Teams.query.filter_by(id=session['id']).first() if team.verified: return redirect(url_for('views.profile')) + else: + verify_email(team.email) return render_template('confirm.html', team=team) @@ -48,7 +57,7 @@ def reset_password(data=None): if data is not None and request.method == "POST": try: s = TimedSerializer(app.config['SECRET_KEY']) - name = s.loads(data.decode('base64'), max_age=1800) + name = s.loads(urllib.unquote(data.decode('base64')), max_age=1800) except BadTimeSignature: return render_template('reset_password.html', errors=['Your link has expired']) team = Teams.query.filter_by(name=name).first() @@ -92,7 +101,7 @@ def register(): emails = Teams.query.add_columns('email', 'id').filter_by(email=email).first() pass_short = len(password) == 0 pass_long = len(password) > 128 - valid_email = re.match("[^@]+@[^@]+\.[^@]+", request.form['email']) + valid_email = re.match(r"(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)", request.form['email']) if not valid_email: errors.append("That email doesn't look right") @@ -121,10 +130,15 @@ def register(): session['admin'] = team.admin session['nonce'] = sha512(os.urandom(10)) - if can_send_mail() and get_config('verify_emails'): - verify_email(team.email) - else: - if can_send_mail(): + if can_send_mail() and get_config('verify_emails'): ## Confirming users is enabled and we can send email. + db.session.close() + logger = logging.getLogger('regs') + logger.warn("[{0}] {1} registered (UNCONFIRMED) with {2}".format(time.strftime("%m/%d/%Y %X"), + request.form['name'].encode('utf-8'), + request.form['email'].encode('utf-8'))) + return redirect(url_for('auth.confirm_user')) + else: ## Don't care about confirming users + if can_send_mail(): ## We want to notify the user that they have registered. sendmail(request.form['email'], "You've successfully registered for {}".format(get_config('ctf_name'))) db.session.close() @@ -142,25 +156,30 @@ def login(): errors = [] name = request.form['name'] team = Teams.query.filter_by(name=name).first() - if team and bcrypt_sha256.verify(request.form['password'], team.password): - try: - session.regenerate() # NO SESSION FIXATION FOR YOU - except: - pass # TODO: Some session objects don't implement regenerate :( - session['username'] = team.name - session['id'] = team.id - session['admin'] = team.admin - session['nonce'] = sha512(os.urandom(10)) - db.session.close() + if team: + if team and bcrypt_sha256.verify(request.form['password'], team.password): + try: + session.regenerate() # NO SESSION FIXATION FOR YOU + except: + pass # TODO: Some session objects don't implement regenerate :( + session['username'] = team.name + session['id'] = team.id + session['admin'] = team.admin + session['nonce'] = sha512(os.urandom(10)) + db.session.close() - logger = logging.getLogger('logins') - logger.warn("[{0}] {1} logged in".format(time.strftime("%m/%d/%Y %X"), session['username'].encode('utf-8'))) + logger = logging.getLogger('logins') + logger.warn("[{0}] {1} logged in".format(time.strftime("%m/%d/%Y %X"), session['username'].encode('utf-8'))) - if request.args.get('next') and is_safe_url(request.args.get('next')): - return redirect(request.args.get('next')) - return redirect(url_for('challenges.challenges_view')) - else: - errors.append("That account doesn't seem to exist") + if request.args.get('next') and is_safe_url(request.args.get('next')): + return redirect(request.args.get('next')) + return redirect(url_for('challenges.challenges_view')) + else: # This user exists but the password is wrong + errors.append("That account doesn't seem to exist") + db.session.close() + return render_template('login.html', errors=errors) + else: # This user just doesn't exist + errors.append("Your username or password is incorrect") db.session.close() return render_template('login.html', errors=errors) else: diff --git a/CTFd/challenges.py b/CTFd/challenges.py index 7b0ad38..f0ed12f 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, is_verified +from CTFd.utils import ctftime, view_after_ctf, authed, unix_time, get_kpm, user_can_view_challenges, is_admin, get_config, get_ip, is_verified, ctf_started, ctf_ended, ctf_name from CTFd.models import db, Challenges, Files, Solves, WrongKeys, Keys, Tags, Teams, Awards from sqlalchemy.sql import and_, or_, not_ @@ -15,16 +15,26 @@ challenges = Blueprint('challenges', __name__) @challenges.route('/challenges', methods=['GET']) def challenges_view(): - if not is_admin(): + errors = [] + start = get_config('start') or 0 + end = get_config('end') or 0 + if not is_admin(): # User is not an admin if not ctftime(): - if view_after_ctf(): + # It is not CTF time + if view_after_ctf(): # But we are allowed to view after the CTF ends pass - else: + else: # We are NOT allowed to view after the CTF ends + errors.append('{} has ended'.format(ctf_name())) + return render_template('chals.html', errors=errors, start=int(start), end=int(end)) return redirect(url_for('views.static_html')) - if get_config('verify_emails') and not is_verified(): + if get_config('verify_emails') and not is_verified(): # User is not confirmed return redirect(url_for('auth.confirm_user')) - if can_view_challenges(): - return render_template('chals.html', ctftime=ctftime()) + if user_can_view_challenges(): # Do we allow unauthenticated users? + if get_config('start') and not ctf_started(): + errors.append('{} has not started yet'.format(ctf_name())) + if (get_config('end') and ctf_ended()) and not view_after_ctf(): + errors.append('{} has ended'.format(ctf_name())) + return render_template('chals.html', errors=errors, start=int(start), end=int(end)) else: return redirect(url_for('auth.login', next='challenges')) @@ -37,7 +47,7 @@ def chals(): pass else: return redirect(url_for('views.static_html')) - if can_view_challenges(): + if user_can_view_challenges(): chals = Challenges.query.filter(or_(Challenges.hidden != True, Challenges.hidden == None)).add_columns('id', 'name', 'value', 'description', 'category').order_by(Challenges.value).all() json = {'game':[]} @@ -55,21 +65,24 @@ def chals(): @challenges.route('/chals/solves') def chals_per_solves(): - if can_view_challenges(): - solves_sub = db.session.query(Solves.chalid, db.func.count(Solves.chalid).label('solves')).join(Teams, Solves.teamid == Teams.id).filter(Teams.banned == False).group_by(Solves.chalid).subquery() - solves = db.session.query(solves_sub.columns.chalid, solves_sub.columns.solves, Challenges.name) \ - .join(Challenges, solves_sub.columns.chalid == Challenges.id).all() - json = {} - for chal, count, name in solves: - json[name] = count - db.session.close() - return jsonify(json) - return redirect(url_for('auth.login', next='chals/solves')) + if not user_can_view_challenges(): + return redirect(url_for('auth.login', next=request.path)) + solves_sub = db.session.query(Solves.chalid, db.func.count(Solves.chalid).label('solves')).join(Teams, Solves.teamid == Teams.id).filter(Teams.banned == False).group_by(Solves.chalid).subquery() + solves = db.session.query(solves_sub.columns.chalid, solves_sub.columns.solves, Challenges.name) \ + .join(Challenges, solves_sub.columns.chalid == Challenges.id).all() + json = {} + for chal, count, name in solves: + json[name] = count + db.session.close() + return jsonify(json) + @challenges.route('/solves') @challenges.route('/solves/') def solves(teamid=None): + if not user_can_view_challenges(): + return redirect(url_for('auth.login', next=request.path)) solves = None awards = None if teamid is None: @@ -109,6 +122,8 @@ def solves(teamid=None): @challenges.route('/maxattempts') def attempts(): + if not user_can_view_challenges(): + return redirect(url_for('auth.login', next=request.path)) chals = Challenges.query.add_columns('id').all() json = {'maxattempts':[]} for chal, chalid in chals: @@ -120,6 +135,8 @@ def attempts(): @challenges.route('/fails/', methods=['GET']) def fails(teamid): + if not user_can_view_challenges(): + return redirect(url_for('auth.login', next=request.path)) fails = WrongKeys.query.filter_by(teamid=teamid).count() solves = Solves.query.filter_by(teamid=teamid).count() db.session.close() @@ -129,6 +146,8 @@ def fails(teamid): @challenges.route('/chal//solves', methods=['GET']) def who_solved(chalid): + if not user_can_view_challenges(): + return redirect(url_for('auth.login', next=request.path)) solves = Solves.query.join(Teams, Solves.teamid == Teams.id).filter(Solves.chalid == chalid, Teams.banned == False).order_by(Solves.date.asc()) json = {'teams':[]} for solve in solves: @@ -138,9 +157,11 @@ def who_solved(chalid): @challenges.route('/chal/', methods=['POST']) def chal(chalid): - if not ctftime(): + if ctf_ended() and not view_after_ctf(): return redirect(url_for('challenges.challenges_view')) - if authed(): + if not user_can_view_challenges(): + return redirect(url_for('auth.login', next=request.path)) + if authed() and is_verified() and (ctf_started() or view_after_ctf()): 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'])) @@ -148,10 +169,11 @@ def chal(chalid): # Anti-bruteforce / submitting keys too quickly if get_kpm(session['id']) > 10: - wrong = WrongKeys(session['id'], chalid, request.form['key']) - db.session.add(wrong) - db.session.commit() - db.session.close() + if ctftime(): + wrong = WrongKeys(session['id'], chalid, request.form['key']) + db.session.add(wrong) + db.session.commit() + db.session.close() logger.warn("[{0}] {1} submitted {2} with kpm {3} [TOO FAST]".format(*data)) # return "3" # Submitting too fast return jsonify({'status': '3', 'message': "You're submitting keys too fast. Slow down."}) @@ -176,28 +198,31 @@ def chal(chalid): if x['type'] == 0: #static key print(x['flag'], key.strip().lower()) if x['flag'] and x['flag'].strip().lower() == key.strip().lower(): - solve = Solves(chalid=chalid, teamid=session['id'], ip=get_ip(), flag=key) - db.session.add(solve) - db.session.commit() - db.session.close() + if ctftime(): + solve = Solves(chalid=chalid, teamid=session['id'], ip=get_ip(), flag=key) + db.session.add(solve) + db.session.commit() + db.session.close() logger.info("[{0}] {1} submitted {2} with kpm {3} [CORRECT]".format(*data)) # return "1" # key was correct return jsonify({'status':'1', 'message':'Correct'}) elif x['type'] == 1: #regex res = re.match(str(x['flag']), key, re.IGNORECASE) if res and res.group() == key: - solve = Solves(chalid=chalid, teamid=session['id'], ip=get_ip(), flag=key) - db.session.add(solve) - db.session.commit() - db.session.close() + if ctftime(): + solve = Solves(chalid=chalid, teamid=session['id'], ip=get_ip(), flag=key) + db.session.add(solve) + db.session.commit() + db.session.close() logger.info("[{0}] {1} submitted {2} with kpm {3} [CORRECT]".format(*data)) # return "1" # key was correct return jsonify({'status': '1', 'message': 'Correct'}) - wrong = WrongKeys(session['id'], chalid, request.form['key']) - db.session.add(wrong) - db.session.commit() - db.session.close() + if ctftime(): + wrong = WrongKeys(session['id'], chalid, request.form['key']) + db.session.add(wrong) + db.session.commit() + db.session.close() logger.info("[{0}] {1} submitted {2} with kpm {3} [WRONG]".format(*data)) # return '0' # key was wrong if max_tries: diff --git a/CTFd/config.py b/CTFd/config.py index c7a3629..22e2e85 100644 --- a/CTFd/config.py +++ b/CTFd/config.py @@ -11,7 +11,7 @@ with open('.ctfd_secret_key', 'a+') as secret: ##### SERVER SETTINGS ##### SECRET_KEY = key -SQLALCHEMY_DATABASE_URI = 'sqlite:///ctfd.db' +SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or 'sqlite:///ctfd.db' SQLALCHEMY_TRACK_MODIFICATIONS = False SESSION_TYPE = "filesystem" SESSION_FILE_DIR = "/tmp/flask_session" @@ -31,3 +31,7 @@ TRUSTED_PROXIES = [ '^172\.(1[6-9]|2[0-9]|3[0-1])\.', '^192\.168\.' ] + +CACHE_TYPE = "simple" +if CACHE_TYPE == 'redis': + CACHE_REDIS_URL = os.environ.get('REDIS_URL') diff --git a/CTFd/models.py b/CTFd/models.py index b7a049e..7c86de4 100644 --- a/CTFd/models.py +++ b/CTFd/models.py @@ -147,6 +147,7 @@ class Teams(db.Model): banned = db.Column(db.Boolean, default=False) verified = db.Column(db.Boolean, default=False) admin = db.Column(db.Boolean, default=False) + joined = db.Column(db.DateTime, default=datetime.datetime.utcnow) def __init__(self, name, email, password): self.name = name diff --git a/CTFd/static/original/admin/js/chalboard.js b/CTFd/static/original/admin/js/chalboard.js index 567ce1f..aafa29e 100644 --- a/CTFd/static/original/admin/js/chalboard.js +++ b/CTFd/static/original/admin/js/chalboard.js @@ -258,7 +258,7 @@ $('#create-key').click(function(e){ var elem = $('
'); elem.append($("
").append($(""))); - elem.append('
Static
'); + elem.append('
Static
'); elem.append('
Regex
'); elem.append('Remove'); diff --git a/CTFd/templates/original/admin/chals.html b/CTFd/templates/original/admin/chals.html index 92f5e73..2adb5d7 100644 --- a/CTFd/templates/original/admin/chals.html +++ b/CTFd/templates/original/admin/chals.html @@ -58,7 +58,7 @@
- +
@@ -90,7 +90,7 @@
- +
@@ -134,7 +134,7 @@
- +
@@ -153,7 +153,7 @@
- +
diff --git a/CTFd/templates/original/admin/teams.html b/CTFd/templates/original/admin/teams.html index 568a29a..871a512 100644 --- a/CTFd/templates/original/admin/teams.html +++ b/CTFd/templates/original/admin/teams.html @@ -67,11 +67,11 @@ input[type="checkbox"] { margin: 0px !important; position: relative; top: 5px; }
- +
- +
diff --git a/CTFd/templates/original/chals.html b/CTFd/templates/original/chals.html index cb98aa9..184828f 100644 --- a/CTFd/templates/original/chals.html +++ b/CTFd/templates/original/chals.html @@ -80,6 +80,17 @@ {% endblock %} {% block content %} + +{% if errors %} +
+
+{% for error in errors %} +

{{ error }}

+{% endfor %} +
+
+{% else %} +

Challenges

@@ -147,10 +158,11 @@
+{% endif %} {% endblock %} {% block scripts %} - + {% if not errors %}{% endif %} {% endblock %} diff --git a/CTFd/templates/original/page.html b/CTFd/templates/original/page.html new file mode 100644 index 0000000..070529a --- /dev/null +++ b/CTFd/templates/original/page.html @@ -0,0 +1,75 @@ + + + + {{ ctf_name() }} + + + + + + + + + + + {% block stylesheets %}{% endblock %} + + + + +
+ + + {{ content | safe }} + +
+ + + +{% block scripts %} +{% endblock %} + + diff --git a/CTFd/templates/original/profile.html b/CTFd/templates/original/profile.html index 6d9f911..9fd28a5 100644 --- a/CTFd/templates/original/profile.html +++ b/CTFd/templates/original/profile.html @@ -43,6 +43,10 @@ @@ -73,7 +77,7 @@ diff --git a/CTFd/utils.py b/CTFd/utils.py index c63f436..f9d7504 100644 --- a/CTFd/utils.py +++ b/CTFd/utils.py @@ -1,9 +1,11 @@ from CTFd.models import db, WrongKeys, Pages, Config, Tracking, Teams, Containers, ip2long, long2ip from six.moves.urllib.parse import urlparse, urljoin +import six from werkzeug.utils import secure_filename from functools import wraps from flask import current_app as app, g, request, redirect, url_for, session, render_template, abort +from flask_cache import Cache from itsdangerous import Signer, BadSignature from socket import inet_aton, inet_ntoa, socket from struct import unpack, pack, error @@ -24,8 +26,11 @@ import smtplib import email import tempfile import subprocess +import urllib import json +cache = Cache() + def init_logs(app): logger_keys = logging.getLogger('keys') @@ -132,11 +137,13 @@ def init_utils(app): abort(403) +@cache.memoize() def ctf_name(): name = get_config('ctf_name') return name if name else 'CTFd' +@cache.memoize() def ctf_theme(): theme = get_config('ctf_theme') return theme if theme else '' @@ -152,13 +159,17 @@ def authed(): def is_verified(): - team = Teams.query.filter_by(id=session.get('id')).first() - if team: - return team.verified + if get_config('verify_emails'): + team = Teams.query.filter_by(id=session.get('id')).first() + if team: + return team.verified + else: + return False else: - return False + return True +@cache.memoize() def is_setup(): setup = Config.query.filter_by(key='setup').first() if setup: @@ -174,12 +185,9 @@ def is_admin(): return False +@cache.memoize() def can_register(): - config = Config.query.filter_by(key='prevent_registration').first() - if config: - return config.value != '1' - else: - return True + return not bool(get_config('prevent_registration')) def admins_only(f): @@ -192,11 +200,9 @@ def admins_only(f): return decorated_function +@cache.memoize() def view_after_ctf(): - if get_config('view_after_ctf') == '1' and time.time() > int(get_config("end")): - return True - else: - return False + return bool(get_config('view_after_ctf')) def ctftime(): @@ -234,10 +240,19 @@ def ctftime(): return False -def can_view_challenges(): - config = Config.query.filter_by(key="view_challenges_unregistered").first() +def ctf_started(): + return time.time() > int(get_config("start") or 0) + + +def ctf_ended(): + return time.time() > int(get_config("end") or 0) + + +def user_can_view_challenges(): + config = bool(get_config('view_challenges_unregistered')) + verify_emails = bool(get_config('verify_emails')) if config: - return authed() or config.value == '1' + return authed() or config else: return authed() @@ -284,14 +299,20 @@ def get_themes(): if os.path.isdir(os.path.join(dir, name))] +@cache.memoize() def get_config(key): config = Config.query.filter_by(key=key).first() if config and config.value: value = config.value if value and value.isdigit(): return int(value) - else: - return value + elif value and isinstance(value, six.string_types): + if value.lower() == 'true': + return True + elif value.lower() == 'false': + return False + else: + return value else: set_config(key, None) return None @@ -308,10 +329,12 @@ def set_config(key, value): return config +@cache.memoize() def can_send_mail(): return mailgun() or mailserver() +@cache.memoize() def mailgun(): if app.config.get('MAILGUN_API_KEY') and app.config.get('MAILGUN_BASE_URL'): return True @@ -319,6 +342,8 @@ def mailgun(): return True return False + +@cache.memoize() def mailserver(): if (get_config('mail_server') and get_config('mail_port')): return True @@ -384,7 +409,7 @@ def verify_email(addr): 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') + url_for('auth.confirm_user', _external=True) + '/' + urllib.quote_plus(token.encode('base64')) ) sendmail(addr, text) @@ -407,6 +432,7 @@ def sha512(string): return hashlib.sha512(string).hexdigest() +@cache.memoize() def can_create_container(): try: output = subprocess.check_output(['docker', 'version']) diff --git a/CTFd/views.py b/CTFd/views.py index c376f10..da494f6 100644 --- a/CTFd/views.py +++ b/CTFd/views.py @@ -1,5 +1,5 @@ from flask import current_app as app, render_template, render_template_string, request, redirect, abort, jsonify, json as json_mod, url_for, session, Blueprint, Response -from CTFd.utils import authed, ip2long, long2ip, is_setup, validate_url, get_config, set_config, sha512, get_ip +from CTFd.utils import authed, ip2long, long2ip, is_setup, validate_url, get_config, set_config, sha512, get_ip, cache from CTFd.models import db, Teams, Solves, Awards, Challenges, WrongKeys, Keys, Tags, Files, Tracking, Pages, Config from jinja2.exceptions import TemplateNotFound @@ -90,6 +90,8 @@ def setup(): db.session.commit() db.session.close() app.setup = False + with app.app_context(): + cache.clear() return redirect(url_for('views.static_html')) return render_template('setup.html', nonce=session.get('nonce')) return redirect(url_for('views.static_html')) @@ -110,7 +112,7 @@ def static_html(template): except TemplateNotFound: page = Pages.query.filter_by(route=template).first() if page: - return render_template_string('{% extends "base.html" %}{% block content %}' + page.html + '{% endblock %}') + return render_template('page.html', content=page.html) else: abort(404) diff --git a/requirements.txt b/requirements.txt index 40c92b9..cda9c81 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,7 @@ Flask Flask-SQLAlchemy Flask-Session +Flask-Caching SQLAlchemy sqlalchemy-utils passlib