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
|
||||
services:
|
||||
- mysql
|
||||
- postgresql
|
||||
env:
|
||||
- TESTING_DATABASE_URL='mysql+pymysql://root@localhost/ctfd'
|
||||
- TESTING_DATABASE_URL='sqlite://'
|
||||
- TESTING_DATABASE_URL='postgres://postgres@localhost/ctfd'
|
||||
python:
|
||||
- 2.7
|
||||
- 3.6
|
||||
install:
|
||||
- pip install -r development.txt
|
||||
before_script:
|
||||
- psql -c 'create database ctfd;' -U postgres
|
||||
script:
|
||||
- pep8 --ignore E501,E712 CTFd/ tests/
|
||||
- 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.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.admin.statistics import admin_statistics
|
||||
|
|
|
@ -16,7 +16,13 @@ admin_challenges = Blueprint('admin_challenges', __name__)
|
|||
def admin_chal_types():
|
||||
data = {}
|
||||
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)
|
||||
|
||||
|
@ -52,7 +58,13 @@ def admin_chals():
|
|||
'max_attempts': x.max_attempts,
|
||||
'type': x.type,
|
||||
'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()
|
||||
|
@ -225,7 +237,14 @@ def admin_create_chal():
|
|||
files = request.files.getlist('files[]')
|
||||
|
||||
# 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:
|
||||
chal.hidden = True
|
||||
else:
|
||||
|
|
|
@ -130,7 +130,9 @@ def chals():
|
|||
'category': x.category,
|
||||
'files': files,
|
||||
'tags': tags,
|
||||
'hints': hints
|
||||
'hints': hints,
|
||||
'template': chal_type.templates['modal'],
|
||||
'script': chal_type.scripts['modal'],
|
||||
})
|
||||
|
||||
db.session.close()
|
||||
|
|
|
@ -127,5 +127,5 @@ class TestingConfig(Config):
|
|||
PRESERVE_CONTEXT_ON_EXCEPTION = False
|
||||
TESTING = True
|
||||
DEBUG = True
|
||||
SQLALCHEMY_DATABASE_URI = 'sqlite://'
|
||||
SQLALCHEMY_DATABASE_URI = os.environ.get('TESTING_DATABASE_URL') or 'sqlite://'
|
||||
SERVER_NAME = 'localhost'
|
||||
|
|
|
@ -48,16 +48,15 @@ class Challenges(db.Model):
|
|||
max_attempts = db.Column(db.Integer, default=0)
|
||||
value = db.Column(db.Integer)
|
||||
category = db.Column(db.String(80))
|
||||
type = db.Column(db.Integer)
|
||||
type = db.Column(db.String(80))
|
||||
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.description = description
|
||||
self.value = value
|
||||
self.category = category
|
||||
self.type = type
|
||||
# self.flags = json.dumps(flags)
|
||||
|
||||
def __repr__(self):
|
||||
return '<chal %r>' % self.name
|
||||
|
@ -190,15 +189,23 @@ class Teams(db.Model):
|
|||
return 0
|
||||
|
||||
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(
|
||||
Solves.teamid.label('teamid'),
|
||||
db.func.sum(Challenges.value).label('score'),
|
||||
db.func.max(Solves.id).label('id'),
|
||||
db.func.max(Solves.date).label('date')
|
||||
).join(Challenges).group_by(Solves.teamid)
|
||||
|
||||
awards = db.session.query(
|
||||
Awards.teamid.label('teamid'),
|
||||
db.func.sum(Awards.value).label('score'),
|
||||
db.func.max(Awards.id).label('id'),
|
||||
db.func.max(Awards.date).label('date')
|
||||
).group_by(Awards.teamid)
|
||||
|
||||
|
@ -212,16 +219,26 @@ class Teams(db.Model):
|
|||
|
||||
results = union_all(scores, awards).alias('results')
|
||||
|
||||
sumscore = db.func.sum(results.columns.score).label('sumscore')
|
||||
quickest = db.func.max(results.columns.date).label('quickest')
|
||||
sumscores = db.session.query(
|
||||
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)\
|
||||
.join(Teams)\
|
||||
.group_by(results.columns.teamid)\
|
||||
.order_by(sumscore.desc(), quickest)
|
||||
|
||||
if not admin:
|
||||
standings_query = standings_query.filter(Teams.banned == False)
|
||||
if admin:
|
||||
standings_query = db.session.query(
|
||||
Teams.id.label('teamid'),
|
||||
)\
|
||||
.join(sumscores, Teams.id == sumscores.columns.teamid) \
|
||||
.order_by(sumscores.columns.score.desc(), sumscores.columns.id)
|
||||
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()
|
||||
|
||||
|
|
|
@ -57,7 +57,7 @@ def init_plugins(app):
|
|||
:return:
|
||||
"""
|
||||
modules = glob.glob(os.path.dirname(__file__) + "/*")
|
||||
blacklist = {'keys', 'challenges', '__pycache__'}
|
||||
blacklist = {'keys', '__pycache__'}
|
||||
for module in modules:
|
||||
module_name = os.path.basename(module)
|
||||
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.models import db, Solves, WrongKeys, Keys
|
||||
from CTFd import utils
|
||||
|
@ -6,14 +7,35 @@ from CTFd import utils
|
|||
class BaseChallenge(object):
|
||||
id = None
|
||||
name = None
|
||||
templates = {}
|
||||
scripts = {}
|
||||
|
||||
|
||||
class CTFdStandardChallenge(BaseChallenge):
|
||||
id = 0
|
||||
name = "standard"
|
||||
id = "standard" # Unique identifier used to register challenges
|
||||
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
|
||||
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()
|
||||
chal_keys = Keys.query.filter_by(chal=chal.id).all()
|
||||
for chal_key in chal_keys:
|
||||
|
@ -23,6 +45,14 @@ class CTFdStandardChallenge(BaseChallenge):
|
|||
|
||||
@staticmethod
|
||||
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()
|
||||
solve = Solves(teamid=team.id, chalid=chal.id, ip=utils.get_ip(req=request), flag=provided_key)
|
||||
db.session.add(solve)
|
||||
|
@ -31,6 +61,14 @@ class CTFdStandardChallenge(BaseChallenge):
|
|||
|
||||
@staticmethod
|
||||
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()
|
||||
wrong = WrongKeys(teamid=team.id, chalid=chal.id, ip=utils.get_ip(request), flag=provided_key)
|
||||
db.session.add(wrong)
|
||||
|
@ -38,13 +76,27 @@ class CTFdStandardChallenge(BaseChallenge):
|
|||
db.session.close()
|
||||
|
||||
|
||||
CHALLENGE_CLASSES = {
|
||||
0: CTFdStandardChallenge
|
||||
}
|
||||
|
||||
|
||||
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)
|
||||
if cls is None:
|
||||
raise KeyError
|
||||
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>
|
||||
<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">
|
||||
<button class="btn btn-theme btn-outlined create-challenge-submit" type="submit">Create</button>
|
||||
</div>
|
|
@ -12,28 +12,48 @@ def get_standings(admin=False, count=None):
|
|||
scores = db.session.query(
|
||||
Solves.teamid.label('teamid'),
|
||||
db.func.sum(Challenges.value).label('score'),
|
||||
db.func.max(Solves.id).label('id'),
|
||||
db.func.max(Solves.date).label('date')
|
||||
).join(Challenges).group_by(Solves.teamid)
|
||||
|
||||
awards = db.session.query(
|
||||
Awards.teamid.label('teamid'),
|
||||
db.func.sum(Awards.value).label('score'),
|
||||
db.func.max(Awards.id).label('id'),
|
||||
db.func.max(Awards.date).label('date')
|
||||
).group_by(Awards.teamid)
|
||||
|
||||
"""
|
||||
Filter out solves and awards that are before a specific time point.
|
||||
"""
|
||||
freeze = utils.get_config('freeze')
|
||||
if not admin and freeze:
|
||||
scores = scores.filter(Solves.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')
|
||||
|
||||
"""
|
||||
Sum each of the results by the team id to get their score.
|
||||
"""
|
||||
sumscores = db.session.query(
|
||||
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()
|
||||
|
||||
"""
|
||||
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:
|
||||
standings_query = db.session.query(
|
||||
Teams.id.label('teamid'),
|
||||
|
@ -41,7 +61,7 @@ def get_standings(admin=False, count=None):
|
|||
Teams.banned, sumscores.columns.score
|
||||
)\
|
||||
.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:
|
||||
standings_query = db.session.query(
|
||||
Teams.id.label('teamid'),
|
||||
|
@ -50,13 +70,17 @@ def get_standings(admin=False, count=None):
|
|||
)\
|
||||
.join(sumscores, Teams.id == sumscores.columns.teamid) \
|
||||
.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:
|
||||
standings = standings_query.all()
|
||||
else:
|
||||
standings = standings_query.limit(count).all()
|
||||
db.session.close()
|
||||
|
||||
return standings
|
||||
|
||||
|
||||
|
|
|
@ -1,24 +1,25 @@
|
|||
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){
|
||||
function load_chal_template(challenge){
|
||||
$.get(script_root + challenge.templates.create, 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(){
|
||||
$.getScript(script_root + challenge.scripts.create, function(){
|
||||
console.log('loaded');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
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]);
|
||||
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);
|
||||
}
|
||||
} else if (chal_type_amt == 1) {
|
||||
|
@ -28,6 +29,6 @@ $.get(script_root + '/admin/chal_types', function(data){
|
|||
}
|
||||
});
|
||||
$('#create-chals-select').change(function(){
|
||||
var chal_type_name = $(this).find("option:selected").text();
|
||||
load_chal_template(chal_type_name);
|
||||
var challenge = $(this).find("option:selected").data('meta');
|
||||
load_chal_template(challenge);
|
||||
});
|
||||
|
|
|
@ -43,11 +43,11 @@ function load_chal_template(id, success_cb){
|
|||
obj = $.grep(challenges['game'], function (e) {
|
||||
return e.id == id;
|
||||
})[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);
|
||||
$("#update-modals-entry-div").html(template({'nonce':$('#nonce').val(), 'script_root':script_root}));
|
||||
$.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",
|
||||
success: success_cb,
|
||||
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/vendor/codemirror.min.js"></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 }}";
|
||||
$.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 src="{{ request.script_root }}/themes/admin/static/js/chal-new.js"></script>
|
||||
{% endblock %}
|
||||
|
|
|
@ -24,7 +24,7 @@ function loadchalbyname(chalname) {
|
|||
}
|
||||
|
||||
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();
|
||||
templates[obj.type] = template_data;
|
||||
var template_data = templates[obj.type];
|
||||
|
@ -46,7 +46,7 @@ function updateChalWindow(obj) {
|
|||
};
|
||||
|
||||
$('#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() {
|
||||
// Handle Solves tab
|
||||
$('.chal-solves').click(function (e) {
|
||||
|
|
|
@ -5,3 +5,4 @@ nose>=1.3.7
|
|||
rednose>=1.1.1
|
||||
pep8>=1.7.0
|
||||
freezegun>=0.3.9
|
||||
psycopg2>=2.7.3.1
|
||||
|
|
|
@ -73,6 +73,7 @@ def run_migrations_online():
|
|||
connection = engine.connect()
|
||||
context.configure(connection=connection,
|
||||
target_metadata=target_metadata,
|
||||
compare_type=True,
|
||||
process_revision_directives=process_revision_directives,
|
||||
**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):
|
||||
with app.app_context():
|
||||
app.db.session.commit()
|
||||
app.db.session.close_all()
|
||||
app.db.drop_all()
|
||||
drop_database(app.config['SQLALCHEMY_DATABASE_URI'])
|
||||
|
||||
|
||||
|
@ -72,11 +76,13 @@ def login_as_user(app, name="user", password="password"):
|
|||
|
||||
def get_scores(user):
|
||||
scores = user.get('/scores')
|
||||
print(scores.get_data(as_text=True))
|
||||
scores = json.loads(scores.get_data(as_text=True))
|
||||
print(scores)
|
||||
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)
|
||||
db.session.add(chal)
|
||||
db.session.commit()
|
||||
|
|
|
@ -309,6 +309,7 @@ def test_register_plugin_script():
|
|||
output = r.get_data(as_text=True)
|
||||
assert '/fake/script/path.js' in output
|
||||
assert 'http://ctfd.io/fake/script/path.js' in output
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_register_plugin_stylesheet():
|
||||
|
@ -322,3 +323,4 @@ def test_register_plugin_stylesheet():
|
|||
output = r.get_data(as_text=True)
|
||||
assert '/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 = json.loads(data)
|
||||
assert data['status'] == -1
|
||||
destroy_ctfd(app)
|
||||
|
|
|
@ -229,47 +229,51 @@ def test_scoring_logic():
|
|||
chal2_id = chal2.id
|
||||
|
||||
# user1 solves chal1
|
||||
with client1.session_transaction() as sess:
|
||||
data = {
|
||||
"key": 'flag',
|
||||
"nonce": sess.get('nonce')
|
||||
}
|
||||
r = client1.post('/chal/{}'.format(chal1_id), data=data)
|
||||
with freeze_time("2017-10-3 03:21:34"):
|
||||
with client1.session_transaction() as sess:
|
||||
data = {
|
||||
"key": 'flag',
|
||||
"nonce": sess.get('nonce')
|
||||
}
|
||||
r = client1.post('/chal/{}'.format(chal1_id), data=data)
|
||||
|
||||
# user1 is now on top
|
||||
scores = get_scores(admin)
|
||||
assert scores[0]['team'] == 'user1'
|
||||
|
||||
# user2 solves chal1 and chal2
|
||||
with client2.session_transaction() as sess:
|
||||
# solve chal1
|
||||
data = {
|
||||
"key": 'flag',
|
||||
"nonce": sess.get('nonce')
|
||||
}
|
||||
r = client2.post('/chal/{}'.format(chal1_id), data=data)
|
||||
# solve chal2
|
||||
data = {
|
||||
"key": 'flag',
|
||||
"nonce": sess.get('nonce')
|
||||
}
|
||||
r = client2.post('/chal/{}'.format(chal2_id), data=data)
|
||||
with freeze_time("2017-10-4 03:30:34"):
|
||||
with client2.session_transaction() as sess:
|
||||
# solve chal1
|
||||
data = {
|
||||
"key": 'flag',
|
||||
"nonce": sess.get('nonce')
|
||||
}
|
||||
r = client2.post('/chal/{}'.format(chal1_id), data=data)
|
||||
# solve chal2
|
||||
data = {
|
||||
"key": 'flag',
|
||||
"nonce": sess.get('nonce')
|
||||
}
|
||||
r = client2.post('/chal/{}'.format(chal2_id), data=data)
|
||||
|
||||
# user2 is now on top
|
||||
scores = get_scores(admin)
|
||||
assert scores[0]['team'] == 'user2'
|
||||
|
||||
# user1 solves chal2
|
||||
with client1.session_transaction() as sess:
|
||||
data = {
|
||||
"key": 'flag',
|
||||
"nonce": sess.get('nonce')
|
||||
}
|
||||
r = client1.post('/chal/{}'.format(chal2_id), data=data)
|
||||
with freeze_time("2017-10-5 03:50:34"):
|
||||
with client1.session_transaction() as sess:
|
||||
data = {
|
||||
"key": 'flag',
|
||||
"nonce": sess.get('nonce')
|
||||
}
|
||||
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)
|
||||
assert scores[0]['team'] == 'user2'
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
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 user2.score() == 105
|
||||
assert user2.place() == '1st'
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_pages_routing_and_rendering():
|
||||
|
@ -457,6 +462,7 @@ def test_user_can_confirm_email(mock_smtp):
|
|||
# The team is now verified
|
||||
team = Teams.query.filter_by(email='user@user.com').first()
|
||||
assert team.verified == True
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
@patch('smtplib.SMTP')
|
||||
|
@ -524,3 +530,4 @@ http://localhost/reset_password/InVzZXIxIi5BZktHUGcuTVhkTmZtOWU2U2xwSXZ1MlFwTjdw
|
|||
# Make sure that the user's password changed
|
||||
team = Teams.query.filter_by(email="user@user.com").first()
|
||||
assert team.password != team_password_saved
|
||||
destroy_ctfd(app)
|
||||
|
|
Loading…
Reference in New Issue