mirror of https://github.com/JohnHammond/CTFd.git
Adopting a challenge type layout from deckar01 (#399)
* Adopting a challenge type layout from deckar01 * Move standard challenge modals into the plugin * Migration to change challenge type id to a string * Travis testing now builds with MySQL, SQLite, and Postgres * Rework get_standings to use the row ID instead of the saved time because of differences in database time precisionselenium-screenshot-testing
parent
faa84ff1e5
commit
608d4f43d9
|
@ -1,9 +1,18 @@
|
||||||
language: python
|
language: python
|
||||||
|
services:
|
||||||
|
- mysql
|
||||||
|
- postgresql
|
||||||
|
env:
|
||||||
|
- TESTING_DATABASE_URL='mysql+pymysql://root@localhost/ctfd'
|
||||||
|
- TESTING_DATABASE_URL='sqlite://'
|
||||||
|
- TESTING_DATABASE_URL='postgres://postgres@localhost/ctfd'
|
||||||
python:
|
python:
|
||||||
- 2.7
|
- 2.7
|
||||||
- 3.6
|
- 3.6
|
||||||
install:
|
install:
|
||||||
- pip install -r development.txt
|
- pip install -r development.txt
|
||||||
|
before_script:
|
||||||
|
- psql -c 'create database ctfd;' -U postgres
|
||||||
script:
|
script:
|
||||||
- pep8 --ignore E501,E712 CTFd/ tests/
|
- pep8 --ignore E501,E712 CTFd/ tests/
|
||||||
- nosetests
|
- nosetests
|
||||||
|
|
|
@ -11,7 +11,6 @@ from sqlalchemy.exc import IntegrityError
|
||||||
|
|
||||||
from CTFd.utils import admins_only, is_admin, cache, export_ctf, import_ctf
|
from CTFd.utils import admins_only, is_admin, cache, export_ctf, import_ctf
|
||||||
from CTFd.models import db, Teams, Solves, Awards, Challenges, WrongKeys, Keys, Tags, Files, Tracking, Pages, Config, DatabaseError
|
from CTFd.models import db, Teams, Solves, Awards, Challenges, WrongKeys, Keys, Tags, Files, Tracking, Pages, Config, DatabaseError
|
||||||
from CTFd.scoreboard import get_standings
|
|
||||||
from CTFd.plugins.keys import get_key_class, KEY_CLASSES
|
from CTFd.plugins.keys import get_key_class, KEY_CLASSES
|
||||||
|
|
||||||
from CTFd.admin.statistics import admin_statistics
|
from CTFd.admin.statistics import admin_statistics
|
||||||
|
|
|
@ -16,7 +16,13 @@ admin_challenges = Blueprint('admin_challenges', __name__)
|
||||||
def admin_chal_types():
|
def admin_chal_types():
|
||||||
data = {}
|
data = {}
|
||||||
for class_id in CHALLENGE_CLASSES:
|
for class_id in CHALLENGE_CLASSES:
|
||||||
data[class_id] = CHALLENGE_CLASSES.get(class_id).name
|
challenge_class = CHALLENGE_CLASSES.get(class_id)
|
||||||
|
data[challenge_class.id] = {
|
||||||
|
'id': challenge_class.id,
|
||||||
|
'name': challenge_class.name,
|
||||||
|
'templates': challenge_class.templates,
|
||||||
|
'scripts': challenge_class.scripts,
|
||||||
|
}
|
||||||
|
|
||||||
return jsonify(data)
|
return jsonify(data)
|
||||||
|
|
||||||
|
@ -52,7 +58,13 @@ def admin_chals():
|
||||||
'max_attempts': x.max_attempts,
|
'max_attempts': x.max_attempts,
|
||||||
'type': x.type,
|
'type': x.type,
|
||||||
'type_name': type_name,
|
'type_name': type_name,
|
||||||
'percentage_solved': percentage
|
'percentage_solved': percentage,
|
||||||
|
'type_data': {
|
||||||
|
'id': type_class.id,
|
||||||
|
'name': type_class.name,
|
||||||
|
'templates': type_class.templates,
|
||||||
|
'scripts': type_class.scripts,
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
db.session.close()
|
db.session.close()
|
||||||
|
@ -225,7 +237,14 @@ def admin_create_chal():
|
||||||
files = request.files.getlist('files[]')
|
files = request.files.getlist('files[]')
|
||||||
|
|
||||||
# Create challenge
|
# Create challenge
|
||||||
chal = Challenges(request.form['name'], request.form['desc'], request.form['value'], request.form['category'], int(request.form['chaltype']))
|
chal = Challenges(
|
||||||
|
name=request.form['name'],
|
||||||
|
description=request.form['desc'],
|
||||||
|
value=request.form['value'],
|
||||||
|
category=request.form['category'],
|
||||||
|
type=request.form['chaltype']
|
||||||
|
)
|
||||||
|
|
||||||
if 'hidden' in request.form:
|
if 'hidden' in request.form:
|
||||||
chal.hidden = True
|
chal.hidden = True
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -130,7 +130,9 @@ def chals():
|
||||||
'category': x.category,
|
'category': x.category,
|
||||||
'files': files,
|
'files': files,
|
||||||
'tags': tags,
|
'tags': tags,
|
||||||
'hints': hints
|
'hints': hints,
|
||||||
|
'template': chal_type.templates['modal'],
|
||||||
|
'script': chal_type.scripts['modal'],
|
||||||
})
|
})
|
||||||
|
|
||||||
db.session.close()
|
db.session.close()
|
||||||
|
|
|
@ -127,5 +127,5 @@ class TestingConfig(Config):
|
||||||
PRESERVE_CONTEXT_ON_EXCEPTION = False
|
PRESERVE_CONTEXT_ON_EXCEPTION = False
|
||||||
TESTING = True
|
TESTING = True
|
||||||
DEBUG = True
|
DEBUG = True
|
||||||
SQLALCHEMY_DATABASE_URI = 'sqlite://'
|
SQLALCHEMY_DATABASE_URI = os.environ.get('TESTING_DATABASE_URL') or 'sqlite://'
|
||||||
SERVER_NAME = 'localhost'
|
SERVER_NAME = 'localhost'
|
||||||
|
|
|
@ -48,16 +48,15 @@ class Challenges(db.Model):
|
||||||
max_attempts = db.Column(db.Integer, default=0)
|
max_attempts = db.Column(db.Integer, default=0)
|
||||||
value = db.Column(db.Integer)
|
value = db.Column(db.Integer)
|
||||||
category = db.Column(db.String(80))
|
category = db.Column(db.String(80))
|
||||||
type = db.Column(db.Integer)
|
type = db.Column(db.String(80))
|
||||||
hidden = db.Column(db.Boolean)
|
hidden = db.Column(db.Boolean)
|
||||||
|
|
||||||
def __init__(self, name, description, value, category, type=0):
|
def __init__(self, name, description, value, category, type='standard'):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.description = description
|
self.description = description
|
||||||
self.value = value
|
self.value = value
|
||||||
self.category = category
|
self.category = category
|
||||||
self.type = type
|
self.type = type
|
||||||
# self.flags = json.dumps(flags)
|
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return '<chal %r>' % self.name
|
return '<chal %r>' % self.name
|
||||||
|
@ -190,15 +189,23 @@ class Teams(db.Model):
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
def place(self, admin=False):
|
def place(self, admin=False):
|
||||||
|
"""
|
||||||
|
This method is generally a clone of CTFd.scoreboard.get_standings.
|
||||||
|
The point being that models.py must be self-reliant and have little
|
||||||
|
to no imports within the CTFd application as importing from the
|
||||||
|
application itself will result in a circular import.
|
||||||
|
"""
|
||||||
scores = db.session.query(
|
scores = db.session.query(
|
||||||
Solves.teamid.label('teamid'),
|
Solves.teamid.label('teamid'),
|
||||||
db.func.sum(Challenges.value).label('score'),
|
db.func.sum(Challenges.value).label('score'),
|
||||||
|
db.func.max(Solves.id).label('id'),
|
||||||
db.func.max(Solves.date).label('date')
|
db.func.max(Solves.date).label('date')
|
||||||
).join(Challenges).group_by(Solves.teamid)
|
).join(Challenges).group_by(Solves.teamid)
|
||||||
|
|
||||||
awards = db.session.query(
|
awards = db.session.query(
|
||||||
Awards.teamid.label('teamid'),
|
Awards.teamid.label('teamid'),
|
||||||
db.func.sum(Awards.value).label('score'),
|
db.func.sum(Awards.value).label('score'),
|
||||||
|
db.func.max(Awards.id).label('id'),
|
||||||
db.func.max(Awards.date).label('date')
|
db.func.max(Awards.date).label('date')
|
||||||
).group_by(Awards.teamid)
|
).group_by(Awards.teamid)
|
||||||
|
|
||||||
|
@ -212,16 +219,26 @@ class Teams(db.Model):
|
||||||
|
|
||||||
results = union_all(scores, awards).alias('results')
|
results = union_all(scores, awards).alias('results')
|
||||||
|
|
||||||
sumscore = db.func.sum(results.columns.score).label('sumscore')
|
sumscores = db.session.query(
|
||||||
quickest = db.func.max(results.columns.date).label('quickest')
|
results.columns.teamid,
|
||||||
|
db.func.sum(results.columns.score).label('score'),
|
||||||
|
db.func.max(results.columns.id).label('id'),
|
||||||
|
db.func.max(results.columns.date).label('date')
|
||||||
|
).group_by(results.columns.teamid).subquery()
|
||||||
|
|
||||||
standings_query = db.session.query(results.columns.teamid)\
|
if admin:
|
||||||
.join(Teams)\
|
standings_query = db.session.query(
|
||||||
.group_by(results.columns.teamid)\
|
Teams.id.label('teamid'),
|
||||||
.order_by(sumscore.desc(), quickest)
|
)\
|
||||||
|
.join(sumscores, Teams.id == sumscores.columns.teamid) \
|
||||||
if not admin:
|
.order_by(sumscores.columns.score.desc(), sumscores.columns.id)
|
||||||
standings_query = standings_query.filter(Teams.banned == False)
|
else:
|
||||||
|
standings_query = db.session.query(
|
||||||
|
Teams.id.label('teamid'),
|
||||||
|
)\
|
||||||
|
.join(sumscores, Teams.id == sumscores.columns.teamid) \
|
||||||
|
.filter(Teams.banned == False) \
|
||||||
|
.order_by(sumscores.columns.score.desc(), sumscores.columns.id)
|
||||||
|
|
||||||
standings = standings_query.all()
|
standings = standings_query.all()
|
||||||
|
|
||||||
|
|
|
@ -57,7 +57,7 @@ def init_plugins(app):
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
modules = glob.glob(os.path.dirname(__file__) + "/*")
|
modules = glob.glob(os.path.dirname(__file__) + "/*")
|
||||||
blacklist = {'keys', 'challenges', '__pycache__'}
|
blacklist = {'keys', '__pycache__'}
|
||||||
for module in modules:
|
for module in modules:
|
||||||
module_name = os.path.basename(module)
|
module_name = os.path.basename(module)
|
||||||
if os.path.isdir(module) and module_name not in blacklist:
|
if os.path.isdir(module) and module_name not in blacklist:
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
from CTFd.plugins import register_plugin_assets_directory
|
||||||
from CTFd.plugins.keys import get_key_class
|
from CTFd.plugins.keys import get_key_class
|
||||||
from CTFd.models import db, Solves, WrongKeys, Keys
|
from CTFd.models import db, Solves, WrongKeys, Keys
|
||||||
from CTFd import utils
|
from CTFd import utils
|
||||||
|
@ -6,14 +7,35 @@ from CTFd import utils
|
||||||
class BaseChallenge(object):
|
class BaseChallenge(object):
|
||||||
id = None
|
id = None
|
||||||
name = None
|
name = None
|
||||||
|
templates = {}
|
||||||
|
scripts = {}
|
||||||
|
|
||||||
|
|
||||||
class CTFdStandardChallenge(BaseChallenge):
|
class CTFdStandardChallenge(BaseChallenge):
|
||||||
id = 0
|
id = "standard" # Unique identifier used to register challenges
|
||||||
name = "standard"
|
name = "standard" # Name of a challenge type
|
||||||
|
templates = { # Handlebars templates used for each aspect of challenge editing & viewing
|
||||||
|
'create': '/plugins/challenges/assets/standard-challenge-create.hbs',
|
||||||
|
'update': '/plugins/challenges/assets/standard-challenge-update.hbs',
|
||||||
|
'modal': '/plugins/challenges/assets/standard-challenge-modal.hbs',
|
||||||
|
}
|
||||||
|
scripts = { # Scripts that are loaded when a template is loaded
|
||||||
|
'create': '/plugins/challenges/assets/standard-challenge-create.js',
|
||||||
|
'update': '/plugins/challenges/assets/standard-challenge-update.js',
|
||||||
|
'modal': '/plugins/challenges/assets/standard-challenge-modal.js',
|
||||||
|
}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def attempt(chal, request):
|
def attempt(chal, request):
|
||||||
|
"""
|
||||||
|
This method is used to check whether a given input is right or wrong. It does not make any changes and should
|
||||||
|
return a boolean for correctness and a string to be shown to the user. It is also in charge of parsing the
|
||||||
|
user's input from the request itself.
|
||||||
|
|
||||||
|
:param chal: The Challenge object from the database
|
||||||
|
:param request: The request the user submitted
|
||||||
|
:return: (boolean, string)
|
||||||
|
"""
|
||||||
provided_key = request.form['key'].strip()
|
provided_key = request.form['key'].strip()
|
||||||
chal_keys = Keys.query.filter_by(chal=chal.id).all()
|
chal_keys = Keys.query.filter_by(chal=chal.id).all()
|
||||||
for chal_key in chal_keys:
|
for chal_key in chal_keys:
|
||||||
|
@ -23,6 +45,14 @@ class CTFdStandardChallenge(BaseChallenge):
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def solve(team, chal, request):
|
def solve(team, chal, request):
|
||||||
|
"""
|
||||||
|
This method is used to insert Solves into the database in order to mark a challenge as solved.
|
||||||
|
|
||||||
|
:param team: The Team object from the database
|
||||||
|
:param chal: The Challenge object from the database
|
||||||
|
:param request: The request the user submitted
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
provided_key = request.form['key'].strip()
|
provided_key = request.form['key'].strip()
|
||||||
solve = Solves(teamid=team.id, chalid=chal.id, ip=utils.get_ip(req=request), flag=provided_key)
|
solve = Solves(teamid=team.id, chalid=chal.id, ip=utils.get_ip(req=request), flag=provided_key)
|
||||||
db.session.add(solve)
|
db.session.add(solve)
|
||||||
|
@ -31,6 +61,14 @@ class CTFdStandardChallenge(BaseChallenge):
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def fail(team, chal, request):
|
def fail(team, chal, request):
|
||||||
|
"""
|
||||||
|
This method is used to insert WrongKeys into the database in order to mark an answer incorrect.
|
||||||
|
|
||||||
|
:param team: The Team object from the database
|
||||||
|
:param chal: The Challenge object from the database
|
||||||
|
:param request: The request the user submitted
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
provided_key = request.form['key'].strip()
|
provided_key = request.form['key'].strip()
|
||||||
wrong = WrongKeys(teamid=team.id, chalid=chal.id, ip=utils.get_ip(request), flag=provided_key)
|
wrong = WrongKeys(teamid=team.id, chalid=chal.id, ip=utils.get_ip(request), flag=provided_key)
|
||||||
db.session.add(wrong)
|
db.session.add(wrong)
|
||||||
|
@ -38,13 +76,27 @@ class CTFdStandardChallenge(BaseChallenge):
|
||||||
db.session.close()
|
db.session.close()
|
||||||
|
|
||||||
|
|
||||||
CHALLENGE_CLASSES = {
|
|
||||||
0: CTFdStandardChallenge
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def get_chal_class(class_id):
|
def get_chal_class(class_id):
|
||||||
|
"""
|
||||||
|
Utility function used to get the corresponding class from a class ID.
|
||||||
|
|
||||||
|
:param class_id: String representing the class ID
|
||||||
|
:return: Challenge class
|
||||||
|
"""
|
||||||
cls = CHALLENGE_CLASSES.get(class_id)
|
cls = CHALLENGE_CLASSES.get(class_id)
|
||||||
if cls is None:
|
if cls is None:
|
||||||
raise KeyError
|
raise KeyError
|
||||||
return cls
|
return cls
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
Global dictionary used to hold all the Challenge Type classes used by CTFd. Insert into this dictionary to register
|
||||||
|
your Challenge Type.
|
||||||
|
"""
|
||||||
|
CHALLENGE_CLASSES = {
|
||||||
|
"standard": CTFdStandardChallenge
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def load(app):
|
||||||
|
register_plugin_assets_directory(app, base_path='/plugins/challenges/assets/')
|
||||||
|
|
|
@ -103,7 +103,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<input type="hidden" value="{{ nonce }}" name="nonce" id="nonce">
|
<input type="hidden" value="{{ nonce }}" name="nonce" id="nonce">
|
||||||
<input type="hidden" value="0" name="chaltype" id="chaltype">
|
<input type="hidden" value="standard" name="chaltype" id="chaltype">
|
||||||
<div style="text-align:center">
|
<div style="text-align:center">
|
||||||
<button class="btn btn-theme btn-outlined create-challenge-submit" type="submit">Create</button>
|
<button class="btn btn-theme btn-outlined create-challenge-submit" type="submit">Create</button>
|
||||||
</div>
|
</div>
|
|
@ -12,28 +12,48 @@ def get_standings(admin=False, count=None):
|
||||||
scores = db.session.query(
|
scores = db.session.query(
|
||||||
Solves.teamid.label('teamid'),
|
Solves.teamid.label('teamid'),
|
||||||
db.func.sum(Challenges.value).label('score'),
|
db.func.sum(Challenges.value).label('score'),
|
||||||
|
db.func.max(Solves.id).label('id'),
|
||||||
db.func.max(Solves.date).label('date')
|
db.func.max(Solves.date).label('date')
|
||||||
).join(Challenges).group_by(Solves.teamid)
|
).join(Challenges).group_by(Solves.teamid)
|
||||||
|
|
||||||
awards = db.session.query(
|
awards = db.session.query(
|
||||||
Awards.teamid.label('teamid'),
|
Awards.teamid.label('teamid'),
|
||||||
db.func.sum(Awards.value).label('score'),
|
db.func.sum(Awards.value).label('score'),
|
||||||
|
db.func.max(Awards.id).label('id'),
|
||||||
db.func.max(Awards.date).label('date')
|
db.func.max(Awards.date).label('date')
|
||||||
).group_by(Awards.teamid)
|
).group_by(Awards.teamid)
|
||||||
|
|
||||||
|
"""
|
||||||
|
Filter out solves and awards that are before a specific time point.
|
||||||
|
"""
|
||||||
freeze = utils.get_config('freeze')
|
freeze = utils.get_config('freeze')
|
||||||
if not admin and freeze:
|
if not admin and freeze:
|
||||||
scores = scores.filter(Solves.date < utils.unix_time_to_utc(freeze))
|
scores = scores.filter(Solves.date < utils.unix_time_to_utc(freeze))
|
||||||
awards = awards.filter(Awards.date < utils.unix_time_to_utc(freeze))
|
awards = awards.filter(Awards.date < utils.unix_time_to_utc(freeze))
|
||||||
|
|
||||||
|
"""
|
||||||
|
Combine awards and solves with a union. They should have the same amount of columns
|
||||||
|
"""
|
||||||
results = union_all(scores, awards).alias('results')
|
results = union_all(scores, awards).alias('results')
|
||||||
|
|
||||||
|
"""
|
||||||
|
Sum each of the results by the team id to get their score.
|
||||||
|
"""
|
||||||
sumscores = db.session.query(
|
sumscores = db.session.query(
|
||||||
results.columns.teamid,
|
results.columns.teamid,
|
||||||
db.func.sum(results.columns.score).label('score'),
|
db.func.sum(results.columns.score).label('score'),
|
||||||
|
db.func.max(results.columns.id).label('id'),
|
||||||
db.func.max(results.columns.date).label('date')
|
db.func.max(results.columns.date).label('date')
|
||||||
).group_by(results.columns.teamid).subquery()
|
).group_by(results.columns.teamid).subquery()
|
||||||
|
|
||||||
|
"""
|
||||||
|
Admins can see scores for all users but the public cannot see banned users.
|
||||||
|
|
||||||
|
Filters out banned users.
|
||||||
|
Properly resolves value ties by ID.
|
||||||
|
|
||||||
|
Different databases treat time precision differently so resolve by the row ID instead.
|
||||||
|
"""
|
||||||
if admin:
|
if admin:
|
||||||
standings_query = db.session.query(
|
standings_query = db.session.query(
|
||||||
Teams.id.label('teamid'),
|
Teams.id.label('teamid'),
|
||||||
|
@ -41,7 +61,7 @@ def get_standings(admin=False, count=None):
|
||||||
Teams.banned, sumscores.columns.score
|
Teams.banned, sumscores.columns.score
|
||||||
)\
|
)\
|
||||||
.join(sumscores, Teams.id == sumscores.columns.teamid) \
|
.join(sumscores, Teams.id == sumscores.columns.teamid) \
|
||||||
.order_by(sumscores.columns.score.desc(), sumscores.columns.date)
|
.order_by(sumscores.columns.score.desc(), sumscores.columns.id)
|
||||||
else:
|
else:
|
||||||
standings_query = db.session.query(
|
standings_query = db.session.query(
|
||||||
Teams.id.label('teamid'),
|
Teams.id.label('teamid'),
|
||||||
|
@ -50,13 +70,17 @@ def get_standings(admin=False, count=None):
|
||||||
)\
|
)\
|
||||||
.join(sumscores, Teams.id == sumscores.columns.teamid) \
|
.join(sumscores, Teams.id == sumscores.columns.teamid) \
|
||||||
.filter(Teams.banned == False) \
|
.filter(Teams.banned == False) \
|
||||||
.order_by(sumscores.columns.score.desc(), sumscores.columns.date)
|
.order_by(sumscores.columns.score.desc(), sumscores.columns.id)
|
||||||
|
|
||||||
|
"""
|
||||||
|
Only select a certain amount of users if asked.
|
||||||
|
"""
|
||||||
if count is None:
|
if count is None:
|
||||||
standings = standings_query.all()
|
standings = standings_query.all()
|
||||||
else:
|
else:
|
||||||
standings = standings_query.limit(count).all()
|
standings = standings_query.limit(count).all()
|
||||||
db.session.close()
|
db.session.close()
|
||||||
|
|
||||||
return standings
|
return standings
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,24 +1,25 @@
|
||||||
function load_chal_template(chal_type_name){
|
function load_chal_template(challenge){
|
||||||
$.get(script_root + '/themes/admin/static/js/templates/challenges/'+ chal_type_name +'/' + chal_type_name + '-challenge-create.hbs', function(template_data){
|
$.get(script_root + challenge.templates.create, function(template_data){
|
||||||
var template = Handlebars.compile(template_data);
|
var template = Handlebars.compile(template_data);
|
||||||
$("#create-chal-entry-div").html(template({'nonce':nonce, 'script_root':script_root}));
|
$("#create-chal-entry-div").html(template({'nonce':nonce, 'script_root':script_root}));
|
||||||
$.getScript(script_root + '/themes/admin/static/js/templates/challenges/'+chal_type_name+'/'+chal_type_name+'-challenge-create.js', function(){
|
$.getScript(script_root + challenge.scripts.create, function(){
|
||||||
console.log('loaded');
|
console.log('loaded');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
nonce = "{{ nonce }}";
|
|
||||||
$.get(script_root + '/admin/chal_types', function(data){
|
$.get(script_root + '/admin/chal_types', function(data){
|
||||||
console.log(data);
|
|
||||||
$("#create-chals-select").empty();
|
$("#create-chals-select").empty();
|
||||||
var chal_type_amt = Object.keys(data).length;
|
var chal_type_amt = Object.keys(data).length;
|
||||||
if (chal_type_amt > 1){
|
if (chal_type_amt > 1){
|
||||||
var option = "<option> -- </option>";
|
var option = "<option> -- </option>";
|
||||||
$("#create-chals-select").append(option);
|
$("#create-chals-select").append(option);
|
||||||
for (var key in data){
|
for (var key in data){
|
||||||
var option = "<option value='{0}'>{1}</option>".format(key, data[key]);
|
var challenge = data[key];
|
||||||
|
var option = $("<option/>");
|
||||||
|
option.attr('value', challenge.type);
|
||||||
|
option.text(challenge.name);
|
||||||
|
option.data('meta', challenge);
|
||||||
$("#create-chals-select").append(option);
|
$("#create-chals-select").append(option);
|
||||||
}
|
}
|
||||||
} else if (chal_type_amt == 1) {
|
} else if (chal_type_amt == 1) {
|
||||||
|
@ -28,6 +29,6 @@ $.get(script_root + '/admin/chal_types', function(data){
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
$('#create-chals-select').change(function(){
|
$('#create-chals-select').change(function(){
|
||||||
var chal_type_name = $(this).find("option:selected").text();
|
var challenge = $(this).find("option:selected").data('meta');
|
||||||
load_chal_template(chal_type_name);
|
load_chal_template(challenge);
|
||||||
});
|
});
|
||||||
|
|
|
@ -43,11 +43,11 @@ function load_chal_template(id, success_cb){
|
||||||
obj = $.grep(challenges['game'], function (e) {
|
obj = $.grep(challenges['game'], function (e) {
|
||||||
return e.id == id;
|
return e.id == id;
|
||||||
})[0]
|
})[0]
|
||||||
$.get(script_root + '/themes/admin/static/js/templates/challenges/'+ obj['type_name'] +'/' + obj['type_name'] + '-challenge-update.hbs', function(template_data){
|
$.get(script_root + obj.type_data.templates.update, function(template_data){
|
||||||
var template = Handlebars.compile(template_data);
|
var template = Handlebars.compile(template_data);
|
||||||
$("#update-modals-entry-div").html(template({'nonce':$('#nonce').val(), 'script_root':script_root}));
|
$("#update-modals-entry-div").html(template({'nonce':$('#nonce').val(), 'script_root':script_root}));
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: script_root + '/themes/admin/static/js/templates/challenges/'+obj['type_name']+'/'+obj['type_name']+'-challenge-update.js',
|
url: script_root + obj.type_data.scripts.update,
|
||||||
dataType: "script",
|
dataType: "script",
|
||||||
success: success_cb,
|
success: success_cb,
|
||||||
cache: true,
|
cache: true,
|
||||||
|
|
|
@ -34,39 +34,7 @@
|
||||||
<script src="{{ request.script_root }}/themes/admin/static/js/utils.js"></script>
|
<script src="{{ request.script_root }}/themes/admin/static/js/utils.js"></script>
|
||||||
<script src="{{ request.script_root }}/themes/admin/static/js/vendor/codemirror.min.js"></script>
|
<script src="{{ request.script_root }}/themes/admin/static/js/vendor/codemirror.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
function load_chal_template(chal_type_name){
|
|
||||||
$.get(script_root + '/themes/admin/static/js/templates/challenges/'+ chal_type_name +'/' + chal_type_name + '-challenge-create.hbs', function(template_data){
|
|
||||||
var template = Handlebars.compile(template_data);
|
|
||||||
$("#create-chal-entry-div").html(template({'nonce':nonce, 'script_root':script_root}));
|
|
||||||
$.getScript(script_root + '/themes/admin/static/js/templates/challenges/'+chal_type_name+'/'+chal_type_name+'-challenge-create.js', function(){
|
|
||||||
console.log('loaded');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
nonce = "{{ nonce }}";
|
nonce = "{{ nonce }}";
|
||||||
$.get(script_root + '/admin/chal_types', function(data){
|
|
||||||
console.log(data);
|
|
||||||
$("#create-chals-select").empty();
|
|
||||||
var chal_type_amt = Object.keys(data).length;
|
|
||||||
if (chal_type_amt > 1){
|
|
||||||
var option = "<option> -- </option>";
|
|
||||||
$("#create-chals-select").append(option);
|
|
||||||
for (var key in data){
|
|
||||||
var option = "<option value='{0}'>{1}</option>".format(key, data[key]);
|
|
||||||
$("#create-chals-select").append(option);
|
|
||||||
}
|
|
||||||
} else if (chal_type_amt == 1) {
|
|
||||||
var key = Object.keys(data)[0];
|
|
||||||
$("#create-chals-select").parent().parent().parent().empty();
|
|
||||||
load_chal_template(data[key]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
$('#create-chals-select').change(function(){
|
|
||||||
var chal_type_name = $(this).find("option:selected").text();
|
|
||||||
load_chal_template(chal_type_name);
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
<script src="{{ request.script_root }}/themes/admin/static/js/chal-new.js"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -24,7 +24,7 @@ function loadchalbyname(chalname) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateChalWindow(obj) {
|
function updateChalWindow(obj) {
|
||||||
$.get(script_root + '/themes/original/static/js/templates/challenges/'+obj.type+'/'+obj.type+'-challenge-modal.hbs', function(template_data){
|
$.get(script_root + obj.template, function(template_data){
|
||||||
$('#chal-window').empty();
|
$('#chal-window').empty();
|
||||||
templates[obj.type] = template_data;
|
templates[obj.type] = template_data;
|
||||||
var template_data = templates[obj.type];
|
var template_data = templates[obj.type];
|
||||||
|
@ -46,7 +46,7 @@ function updateChalWindow(obj) {
|
||||||
};
|
};
|
||||||
|
|
||||||
$('#chal-window').append(template(wrapper));
|
$('#chal-window').append(template(wrapper));
|
||||||
$.getScript(script_root + '/themes/original/static/js/templates/challenges/'+obj.type+'/'+obj.type+'-challenge-script.js',
|
$.getScript(script_root + obj.script,
|
||||||
function() {
|
function() {
|
||||||
// Handle Solves tab
|
// Handle Solves tab
|
||||||
$('.chal-solves').click(function (e) {
|
$('.chal-solves').click(function (e) {
|
||||||
|
|
|
@ -5,3 +5,4 @@ nose>=1.3.7
|
||||||
rednose>=1.1.1
|
rednose>=1.1.1
|
||||||
pep8>=1.7.0
|
pep8>=1.7.0
|
||||||
freezegun>=0.3.9
|
freezegun>=0.3.9
|
||||||
|
psycopg2>=2.7.3.1
|
||||||
|
|
|
@ -73,6 +73,7 @@ def run_migrations_online():
|
||||||
connection = engine.connect()
|
connection = engine.connect()
|
||||||
context.configure(connection=connection,
|
context.configure(connection=connection,
|
||||||
target_metadata=target_metadata,
|
target_metadata=target_metadata,
|
||||||
|
compare_type=True,
|
||||||
process_revision_directives=process_revision_directives,
|
process_revision_directives=process_revision_directives,
|
||||||
**current_app.extensions['migrate'].configure_args)
|
**current_app.extensions['migrate'].configure_args)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,64 @@
|
||||||
|
"""Use strings for challenge types
|
||||||
|
|
||||||
|
Revision ID: 7e9efd084c5a
|
||||||
|
Revises: cbf5620f8e15
|
||||||
|
Create Date: 2017-10-04 16:40:16.508879
|
||||||
|
|
||||||
|
"""
|
||||||
|
from CTFd.models import db, Challenges
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
from sqlalchemy.sql import text, table, column
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '7e9efd084c5a'
|
||||||
|
down_revision = 'cbf5620f8e15'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
challenges_table = table('challenges',
|
||||||
|
column('type', db.Integer),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
bind = op.get_bind()
|
||||||
|
url = str(bind.engine.url)
|
||||||
|
if url.startswith('mysql'):
|
||||||
|
op.alter_column('challenges', 'type',
|
||||||
|
existing_type=sa.INTEGER(),
|
||||||
|
type_=sa.String(length=80),
|
||||||
|
existing_nullable=True)
|
||||||
|
|
||||||
|
op.execute("UPDATE challenges set type='standard' WHERE type=0")
|
||||||
|
elif url.startswith('postgres'):
|
||||||
|
op.alter_column('challenges', 'type',
|
||||||
|
existing_type=sa.INTEGER(),
|
||||||
|
type_=sa.String(length=80),
|
||||||
|
existing_nullable=True,
|
||||||
|
postgresql_using="COALESCE(NULLIF(type, 0)::CHARACTER, 'standard')"
|
||||||
|
)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
bind = op.get_bind()
|
||||||
|
url = str(bind.engine.url)
|
||||||
|
if url.startswith('mysql'):
|
||||||
|
op.execute("UPDATE challenges set type=0 WHERE type='standard'")
|
||||||
|
|
||||||
|
op.alter_column('challenges', 'type',
|
||||||
|
existing_type=sa.String(length=80),
|
||||||
|
type_=sa.INTEGER(),
|
||||||
|
existing_nullable=True)
|
||||||
|
elif url.startswith('postgres'):
|
||||||
|
op.alter_column('challenges', 'type',
|
||||||
|
existing_type=sa.String(length=80),
|
||||||
|
type_=sa.INTEGER(),
|
||||||
|
existing_nullable=True,
|
||||||
|
postgresql_using="COALESCE(NULLIF(type, 'standard')::NUMERIC, 0)"
|
||||||
|
)
|
||||||
|
# ### end Alembic commands ###
|
|
@ -39,6 +39,10 @@ def setup_ctfd(app, ctf_name="CTFd", name="admin", email="admin@ctfd.io", passwo
|
||||||
|
|
||||||
|
|
||||||
def destroy_ctfd(app):
|
def destroy_ctfd(app):
|
||||||
|
with app.app_context():
|
||||||
|
app.db.session.commit()
|
||||||
|
app.db.session.close_all()
|
||||||
|
app.db.drop_all()
|
||||||
drop_database(app.config['SQLALCHEMY_DATABASE_URI'])
|
drop_database(app.config['SQLALCHEMY_DATABASE_URI'])
|
||||||
|
|
||||||
|
|
||||||
|
@ -72,11 +76,13 @@ def login_as_user(app, name="user", password="password"):
|
||||||
|
|
||||||
def get_scores(user):
|
def get_scores(user):
|
||||||
scores = user.get('/scores')
|
scores = user.get('/scores')
|
||||||
|
print(scores.get_data(as_text=True))
|
||||||
scores = json.loads(scores.get_data(as_text=True))
|
scores = json.loads(scores.get_data(as_text=True))
|
||||||
|
print(scores)
|
||||||
return scores['standings']
|
return scores['standings']
|
||||||
|
|
||||||
|
|
||||||
def gen_challenge(db, name='chal_name', description='chal_description', value=100, category='chal_category', type=0):
|
def gen_challenge(db, name='chal_name', description='chal_description', value=100, category='chal_category', type='standard'):
|
||||||
chal = Challenges(name, description, value, category)
|
chal = Challenges(name, description, value, category)
|
||||||
db.session.add(chal)
|
db.session.add(chal)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
|
@ -309,6 +309,7 @@ def test_register_plugin_script():
|
||||||
output = r.get_data(as_text=True)
|
output = r.get_data(as_text=True)
|
||||||
assert '/fake/script/path.js' in output
|
assert '/fake/script/path.js' in output
|
||||||
assert 'http://ctfd.io/fake/script/path.js' in output
|
assert 'http://ctfd.io/fake/script/path.js' in output
|
||||||
|
destroy_ctfd(app)
|
||||||
|
|
||||||
|
|
||||||
def test_register_plugin_stylesheet():
|
def test_register_plugin_stylesheet():
|
||||||
|
@ -322,3 +323,4 @@ def test_register_plugin_stylesheet():
|
||||||
output = r.get_data(as_text=True)
|
output = r.get_data(as_text=True)
|
||||||
assert '/fake/stylesheet/path.css' in output
|
assert '/fake/stylesheet/path.css' in output
|
||||||
assert 'http://ctfd.io/fake/stylesheet/path.css' in output
|
assert 'http://ctfd.io/fake/stylesheet/path.css' in output
|
||||||
|
destroy_ctfd(app)
|
||||||
|
|
|
@ -268,3 +268,4 @@ def test_that_view_challenges_unregistered_works():
|
||||||
data = r.get_data(as_text=True)
|
data = r.get_data(as_text=True)
|
||||||
data = json.loads(data)
|
data = json.loads(data)
|
||||||
assert data['status'] == -1
|
assert data['status'] == -1
|
||||||
|
destroy_ctfd(app)
|
||||||
|
|
|
@ -229,47 +229,51 @@ def test_scoring_logic():
|
||||||
chal2_id = chal2.id
|
chal2_id = chal2.id
|
||||||
|
|
||||||
# user1 solves chal1
|
# user1 solves chal1
|
||||||
with client1.session_transaction() as sess:
|
with freeze_time("2017-10-3 03:21:34"):
|
||||||
data = {
|
with client1.session_transaction() as sess:
|
||||||
"key": 'flag',
|
data = {
|
||||||
"nonce": sess.get('nonce')
|
"key": 'flag',
|
||||||
}
|
"nonce": sess.get('nonce')
|
||||||
r = client1.post('/chal/{}'.format(chal1_id), data=data)
|
}
|
||||||
|
r = client1.post('/chal/{}'.format(chal1_id), data=data)
|
||||||
|
|
||||||
# user1 is now on top
|
# user1 is now on top
|
||||||
scores = get_scores(admin)
|
scores = get_scores(admin)
|
||||||
assert scores[0]['team'] == 'user1'
|
assert scores[0]['team'] == 'user1'
|
||||||
|
|
||||||
# user2 solves chal1 and chal2
|
# user2 solves chal1 and chal2
|
||||||
with client2.session_transaction() as sess:
|
with freeze_time("2017-10-4 03:30:34"):
|
||||||
# solve chal1
|
with client2.session_transaction() as sess:
|
||||||
data = {
|
# solve chal1
|
||||||
"key": 'flag',
|
data = {
|
||||||
"nonce": sess.get('nonce')
|
"key": 'flag',
|
||||||
}
|
"nonce": sess.get('nonce')
|
||||||
r = client2.post('/chal/{}'.format(chal1_id), data=data)
|
}
|
||||||
# solve chal2
|
r = client2.post('/chal/{}'.format(chal1_id), data=data)
|
||||||
data = {
|
# solve chal2
|
||||||
"key": 'flag',
|
data = {
|
||||||
"nonce": sess.get('nonce')
|
"key": 'flag',
|
||||||
}
|
"nonce": sess.get('nonce')
|
||||||
r = client2.post('/chal/{}'.format(chal2_id), data=data)
|
}
|
||||||
|
r = client2.post('/chal/{}'.format(chal2_id), data=data)
|
||||||
|
|
||||||
# user2 is now on top
|
# user2 is now on top
|
||||||
scores = get_scores(admin)
|
scores = get_scores(admin)
|
||||||
assert scores[0]['team'] == 'user2'
|
assert scores[0]['team'] == 'user2'
|
||||||
|
|
||||||
# user1 solves chal2
|
# user1 solves chal2
|
||||||
with client1.session_transaction() as sess:
|
with freeze_time("2017-10-5 03:50:34"):
|
||||||
data = {
|
with client1.session_transaction() as sess:
|
||||||
"key": 'flag',
|
data = {
|
||||||
"nonce": sess.get('nonce')
|
"key": 'flag',
|
||||||
}
|
"nonce": sess.get('nonce')
|
||||||
r = client1.post('/chal/{}'.format(chal2_id), data=data)
|
}
|
||||||
|
r = client1.post('/chal/{}'.format(chal2_id), data=data)
|
||||||
|
|
||||||
# user should still be on top because they solved chal2 first
|
# user2 should still be on top because they solved chal2 first
|
||||||
scores = get_scores(admin)
|
scores = get_scores(admin)
|
||||||
assert scores[0]['team'] == 'user2'
|
assert scores[0]['team'] == 'user2'
|
||||||
|
destroy_ctfd(app)
|
||||||
|
|
||||||
|
|
||||||
def test_user_score_is_correct():
|
def test_user_score_is_correct():
|
||||||
|
@ -309,6 +313,7 @@ def test_user_score_is_correct():
|
||||||
# assert that user2's score is now 105 and is in 1st place
|
# assert that user2's score is now 105 and is in 1st place
|
||||||
assert user2.score() == 105
|
assert user2.score() == 105
|
||||||
assert user2.place() == '1st'
|
assert user2.place() == '1st'
|
||||||
|
destroy_ctfd(app)
|
||||||
|
|
||||||
|
|
||||||
def test_pages_routing_and_rendering():
|
def test_pages_routing_and_rendering():
|
||||||
|
@ -457,6 +462,7 @@ def test_user_can_confirm_email(mock_smtp):
|
||||||
# The team is now verified
|
# The team is now verified
|
||||||
team = Teams.query.filter_by(email='user@user.com').first()
|
team = Teams.query.filter_by(email='user@user.com').first()
|
||||||
assert team.verified == True
|
assert team.verified == True
|
||||||
|
destroy_ctfd(app)
|
||||||
|
|
||||||
|
|
||||||
@patch('smtplib.SMTP')
|
@patch('smtplib.SMTP')
|
||||||
|
@ -524,3 +530,4 @@ http://localhost/reset_password/InVzZXIxIi5BZktHUGcuTVhkTmZtOWU2U2xwSXZ1MlFwTjdw
|
||||||
# Make sure that the user's password changed
|
# Make sure that the user's password changed
|
||||||
team = Teams.query.filter_by(email="user@user.com").first()
|
team = Teams.query.filter_by(email="user@user.com").first()
|
||||||
assert team.password != team_password_saved
|
assert team.password != team_password_saved
|
||||||
|
destroy_ctfd(app)
|
||||||
|
|
Loading…
Reference in New Issue