Merge Dev into Master (#591)

* Chals endpoint seperation (#572)

* Separate the logic of ctftime and email confirmations and admin checking into decorators
* Separate the chals endpoint into /chals and /chals/:id. Closes #552, #435.
* Challenges are now loaded directly from the server before being displayed to the user. 
* Challenge modals now use `{{ description }}` instead of `{{ desc }}`.
* 403 is now a more common status code and can indicate that a CTF has not begun or that you are not logged in. This is in addition to CSRF failures. 
* Update tests to new behavior

* Fixing glitch if an entry chal or team id isn't defined

* Markdown it (#574)

* Replace Marked with Markdown-It

* Update modal change (#576)

* Switch update modals to use nunjucks instead of JS to load in data. 
* Fix previewing challenges after hitting the challenge update button.
* Fix edit-files issue with an unnecessary request.

* Fix solves button

* Closes #592
selenium-screenshot-testing
Kevin Chung 2018-04-16 00:24:04 -04:00 committed by GitHub
parent d17e599193
commit 51d098080f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 575 additions and 403 deletions

View File

@ -11,16 +11,22 @@ from CTFd.plugins.keys import get_key_class
from CTFd.plugins.challenges import get_chal_class from CTFd.plugins.challenges import get_chal_class
from CTFd import utils from CTFd import utils
from CTFd.utils.decorators import (
authed_only,
admins_only,
during_ctf_time_only,
require_verified_emails,
viewable_without_authentication
)
from CTFd.utils import text_type from CTFd.utils import text_type
challenges = Blueprint('challenges', __name__) challenges = Blueprint('challenges', __name__)
@challenges.route('/hints/<int:hintid>', methods=['GET', 'POST']) @challenges.route('/hints/<int:hintid>', methods=['GET', 'POST'])
@during_ctf_time_only
@authed_only
def hints_view(hintid): def hints_view(hintid):
if utils.ctf_started() is False:
if utils.is_admin() is False:
abort(403)
hint = Hints.query.filter_by(id=hintid).first_or_404() hint = Hints.query.filter_by(id=hintid).first_or_404()
chal = Challenges.query.filter_by(id=hint.chal).first() chal = Challenges.query.filter_by(id=hint.chal).first()
unlock = Unlocks.query.filter_by(model='hints', itemid=hintid, teamid=session['id']).first() unlock = Unlocks.query.filter_by(model='hints', itemid=hintid, teamid=session['id']).first()
@ -68,14 +74,18 @@ def hints_view(hintid):
@challenges.route('/challenges', methods=['GET']) @challenges.route('/challenges', methods=['GET'])
@during_ctf_time_only
@require_verified_emails
@viewable_without_authentication()
def challenges_view(): def challenges_view():
infos = [] infos = []
errors = [] errors = []
start = utils.get_config('start') or 0 start = utils.get_config('start') or 0
end = utils.get_config('end') or 0 end = utils.get_config('end') or 0
if utils.ctf_paused(): if utils.ctf_paused():
infos.append('{} is paused'.format(utils.ctf_name())) infos.append('{} is paused'.format(utils.ctf_name()))
if not utils.is_admin(): # User is not an admin
if not utils.ctftime(): if not utils.ctftime():
# It is not CTF time # It is not CTF time
if utils.view_after_ctf(): # But we are allowed to view after the CTF ends if utils.view_after_ctf(): # But we are allowed to view after the CTF ends
@ -87,76 +97,68 @@ def challenges_view():
errors.append('{} has ended'.format(utils.ctf_name())) errors.append('{} has ended'.format(utils.ctf_name()))
return render_template('challenges.html', infos=infos, errors=errors, start=int(start), end=int(end)) return render_template('challenges.html', infos=infos, errors=errors, start=int(start), end=int(end))
if utils.get_config('verify_emails'):
if utils.authed():
if utils.is_admin() is False and utils.is_verified() is False: # User is not confirmed
return redirect(url_for('auth.confirm_user'))
if utils.user_can_view_challenges(): # Do we allow unauthenticated users?
if utils.get_config('start') and not utils.ctf_started():
errors.append('{} has not started yet'.format(utils.ctf_name()))
if (utils.get_config('end') and utils.ctf_ended()) and not utils.view_after_ctf():
errors.append('{} has ended'.format(utils.ctf_name()))
return render_template('challenges.html', infos=infos, errors=errors, start=int(start), end=int(end)) return render_template('challenges.html', infos=infos, errors=errors, start=int(start), end=int(end))
else:
return redirect(url_for('auth.login', next='challenges'))
@challenges.route('/chals', methods=['GET']) @challenges.route('/chals', methods=['GET'])
@during_ctf_time_only
@require_verified_emails
@viewable_without_authentication(status_code=403)
def chals(): def chals():
if not utils.is_admin(): db_chals = Challenges.query.filter(or_(Challenges.hidden != True, Challenges.hidden == None)).order_by(Challenges.value).all()
if not utils.ctftime(): response = {'game': []}
if utils.view_after_ctf(): for chal in db_chals:
pass tags = [tag.tag for tag in Tags.query.add_columns('tag').filter_by(chal=chal.id).all()]
else: chal_type = get_chal_class(chal.type)
abort(403) response['game'].append({
'id': chal.id,
if utils.get_config('verify_emails'):
if utils.authed():
if utils.is_admin() is False and utils.is_verified() is False: # User is not confirmed
abort(403)
if utils.user_can_view_challenges() and (utils.ctf_started() or utils.is_admin()):
teamid = session.get('id')
chals = Challenges.query.filter(or_(Challenges.hidden != True, Challenges.hidden == None)).order_by(Challenges.value).all()
json = {'game': []}
for x in chals:
tags = [tag.tag for tag in Tags.query.add_columns('tag').filter_by(chal=x.id).all()]
files = [str(f.location) for f in Files.query.filter_by(chal=x.id).all()]
unlocked_hints = set([u.itemid for u in Unlocks.query.filter_by(model='hints', teamid=teamid)])
hints = []
for hint in Hints.query.filter_by(chal=x.id).all():
if hint.id in unlocked_hints or utils.ctf_ended():
hints.append({'id': hint.id, 'cost': hint.cost, 'hint': hint.hint})
else:
hints.append({'id': hint.id, 'cost': hint.cost})
chal_type = get_chal_class(x.type)
json['game'].append({
'id': x.id,
'type': chal_type.name, 'type': chal_type.name,
'name': x.name, 'name': chal.name,
'value': x.value, 'value': chal.value,
'description': x.description, 'category': chal.category,
'category': x.category,
'files': files,
'tags': tags, 'tags': tags,
'hints': hints,
'template': chal_type.templates['modal'], 'template': chal_type.templates['modal'],
'script': chal_type.scripts['modal'], 'script': chal_type.scripts['modal'],
}) })
db.session.close() db.session.close()
return jsonify(json) return jsonify(response)
@challenges.route('/chals/<int:chal_id>', methods=['GET'])
@during_ctf_time_only
@require_verified_emails
@viewable_without_authentication(status_code=403)
def chal_view(chal_id):
teamid = session.get('id')
chal = Challenges.query.filter_by(id=chal_id).first_or_404()
chal_class = get_chal_class(chal.type)
tags = [tag.tag for tag in Tags.query.add_columns('tag').filter_by(chal=chal.id).all()]
files = [str(f.location) for f in Files.query.filter_by(chal=chal.id).all()]
unlocked_hints = set([u.itemid for u in Unlocks.query.filter_by(model='hints', teamid=teamid)])
hints = []
for hint in Hints.query.filter_by(chal=chal.id).all():
if hint.id in unlocked_hints or utils.ctf_ended():
hints.append({'id': hint.id, 'cost': hint.cost, 'hint': hint.hint})
else: else:
hints.append({'id': hint.id, 'cost': hint.cost})
challenge, response = chal_class.read(challenge=chal)
response['files'] = files
response['tags'] = tags
response['hints'] = hints
db.session.close() db.session.close()
abort(403) return jsonify(response)
@challenges.route('/chals/solves') @challenges.route('/chals/solves')
@viewable_without_authentication(status_code=403)
def solves_per_chal(): def solves_per_chal():
if not utils.user_can_view_challenges():
return redirect(url_for('auth.login', next=request.path))
chals = Challenges.query\ chals = Challenges.query\
.filter(or_(Challenges.hidden != True, Challenges.hidden == None))\ .filter(or_(Challenges.hidden != True, Challenges.hidden == None))\
.order_by(Challenges.value)\ .order_by(Challenges.value)\
@ -195,21 +197,54 @@ def solves_per_chal():
@challenges.route('/solves') @challenges.route('/solves')
@challenges.route('/solves/<int:teamid>') @authed_only
def solves(teamid=None): def solves_private():
solves = None solves = None
awards = None awards = None
if teamid is None:
if utils.is_admin(): if utils.is_admin():
solves = Solves.query.filter_by(teamid=session['id']).all() solves = Solves.query.filter_by(teamid=session['id']).all()
elif utils.user_can_view_challenges(): elif utils.user_can_view_challenges():
if utils.authed(): if utils.authed():
solves = Solves.query.join(Teams, Solves.teamid == Teams.id).filter(Solves.teamid == session['id'], Teams.banned == False).all() solves = Solves.query\
.join(Teams, Solves.teamid == Teams.id)\
.filter(Solves.teamid == session['id'], Teams.banned == False)\
.all()
else: else:
return jsonify({'solves': []}) return jsonify({'solves': []})
else: else:
return redirect(url_for('auth.login', next='solves')) return redirect(url_for('auth.login', next='solves'))
else:
db.session.close()
response = {'solves': []}
for solve in solves:
response['solves'].append({
'chal': solve.chal.name,
'chalid': solve.chalid,
'team': solve.teamid,
'value': solve.chal.value,
'category': solve.chal.category,
'time': utils.unix_time(solve.date)
})
if awards:
for award in awards:
response['solves'].append({
'chal': award.name,
'chalid': None,
'team': award.teamid,
'value': award.value,
'category': award.category or "Award",
'time': utils.unix_time(award.date)
})
response['solves'].sort(key=lambda k: k['time'])
return jsonify(response)
@challenges.route('/solves/<int:teamid>')
def solves_public(teamid=None):
solves = None
awards = None
if utils.authed() and session['id'] == teamid: if utils.authed() and session['id'] == teamid:
solves = Solves.query.filter_by(teamid=teamid) solves = Solves.query.filter_by(teamid=teamid)
awards = Awards.query.filter_by(teamid=teamid) awards = Awards.query.filter_by(teamid=teamid)
@ -241,9 +276,10 @@ def solves(teamid=None):
solves = solves.all() solves = solves.all()
awards = awards.all() awards = awards.all()
db.session.close() db.session.close()
json = {'solves': []}
response = {'solves': []}
for solve in solves: for solve in solves:
json['solves'].append({ response['solves'].append({
'chal': solve.chal.name, 'chal': solve.chal.name,
'chalid': solve.chalid, 'chalid': solve.chalid,
'team': solve.teamid, 'team': solve.teamid,
@ -253,7 +289,7 @@ def solves(teamid=None):
}) })
if awards: if awards:
for award in awards: for award in awards:
json['solves'].append({ response['solves'].append({
'chal': award.name, 'chal': award.name,
'chalid': None, 'chalid': None,
'team': award.teamid, 'team': award.teamid,
@ -261,30 +297,26 @@ def solves(teamid=None):
'category': award.category or "Award", 'category': award.category or "Award",
'time': utils.unix_time(award.date) 'time': utils.unix_time(award.date)
}) })
json['solves'].sort(key=lambda k: k['time']) response['solves'].sort(key=lambda k: k['time'])
return jsonify(json) return jsonify(response)
@challenges.route('/maxattempts')
def attempts():
if not utils.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:
fails = WrongKeys.query.filter_by(teamid=session['id'], chalid=chalid).count()
if fails >= int(utils.get_config("max_tries")) and int(utils.get_config("max_tries")) > 0:
json['maxattempts'].append({'chalid': chalid})
return jsonify(json)
@challenges.route('/fails') @challenges.route('/fails')
@challenges.route('/fails/<int:teamid>') @authed_only
def fails(teamid=None): def fails_private():
if teamid is None:
fails = WrongKeys.query.filter_by(teamid=session['id']).count() fails = WrongKeys.query.filter_by(teamid=session['id']).count()
solves = Solves.query.filter_by(teamid=session['id']).count() solves = Solves.query.filter_by(teamid=session['id']).count()
else:
db.session.close()
response = {
'fails': str(fails),
'solves': str(solves)
}
return jsonify(response)
@challenges.route('/fails/<int:teamid>')
def fails_public(teamid=None):
if utils.authed() and session['id'] == teamid: if utils.authed() and session['id'] == teamid:
fails = WrongKeys.query.filter_by(teamid=teamid).count() fails = WrongKeys.query.filter_by(teamid=teamid).count()
solves = Solves.query.filter_by(teamid=teamid).count() solves = Solves.query.filter_by(teamid=teamid).count()
@ -295,35 +327,35 @@ def fails(teamid=None):
fails = WrongKeys.query.filter_by(teamid=teamid).count() fails = WrongKeys.query.filter_by(teamid=teamid).count()
solves = Solves.query.filter_by(teamid=teamid).count() solves = Solves.query.filter_by(teamid=teamid).count()
db.session.close() db.session.close()
json = {'fails': str(fails), 'solves': str(solves)} response = {
return jsonify(json) 'fails': str(fails),
'solves': str(solves)
}
return jsonify(response)
@challenges.route('/chal/<int:chalid>/solves', methods=['GET']) @challenges.route('/chal/<int:chalid>/solves', methods=['GET'])
@during_ctf_time_only
@viewable_without_authentication(status_code=403)
def who_solved(chalid): def who_solved(chalid):
if not utils.user_can_view_challenges(): response = {'teams': []}
return redirect(url_for('auth.login', next=request.path))
json = {'teams': []}
if utils.hide_scores(): if utils.hide_scores():
return jsonify(json) return jsonify(response)
solves = Solves.query.join(Teams, Solves.teamid == Teams.id).filter(Solves.chalid == chalid, Teams.banned == False).order_by(Solves.date.asc()) solves = Solves.query.join(Teams, Solves.teamid == Teams.id).filter(Solves.chalid == chalid, Teams.banned == False).order_by(Solves.date.asc())
for solve in solves: for solve in solves:
json['teams'].append({'id': solve.team.id, 'name': solve.team.name, 'date': solve.date}) response['teams'].append({'id': solve.team.id, 'name': solve.team.name, 'date': solve.date})
return jsonify(json) return jsonify(response)
@challenges.route('/chal/<int:chalid>', methods=['POST']) @challenges.route('/chal/<int:chalid>', methods=['POST'])
@during_ctf_time_only
@viewable_without_authentication()
def chal(chalid): def chal(chalid):
if utils.ctf_paused(): if utils.ctf_paused():
return jsonify({ return jsonify({
'status': 3, 'status': 3,
'message': '{} is paused'.format(utils.ctf_name()) 'message': '{} is paused'.format(utils.ctf_name())
}) })
if utils.ctf_ended() and not utils.view_after_ctf():
abort(403)
if not utils.user_can_view_challenges():
return redirect(url_for('auth.login', next=request.path))
if (utils.authed() and utils.is_verified() and (utils.ctf_started() or utils.view_after_ctf())) or utils.is_admin(): if (utils.authed() and utils.is_verified() and (utils.ctf_started() or utils.view_after_ctf())) or utils.is_admin():
team = Teams.query.filter_by(id=session['id']).first() team = Teams.query.filter_by(id=session['id']).first()
fails = WrongKeys.query.filter_by(teamid=session['id'], chalid=chalid).count() fails = WrongKeys.query.filter_by(teamid=session['id'], chalid=chalid).count()

View File

@ -1,12 +1,24 @@
// Markdown Preview // Markdown Preview
$('#desc-edit').on('shown.bs.tab', function (event) { $('#desc-edit').on('shown.bs.tab', function (event) {
var md = window.markdownit({
html: true,
});
if (event.target.hash == '#desc-preview'){ if (event.target.hash == '#desc-preview'){
$(event.target.hash).html(marked($('#desc-editor').val(), {'gfm':true, 'breaks':true})); var editor_value = $('#desc-editor').val();
$(event.target.hash).html(
md.render(editor_value)
);
} }
}); });
$('#new-desc-edit').on('shown.bs.tab', function (event) { $('#new-desc-edit').on('shown.bs.tab', function (event) {
var md = window.markdownit({
html: true,
});
if (event.target.hash == '#new-desc-preview'){ if (event.target.hash == '#new-desc-preview'){
$(event.target.hash).html(marked($('#new-desc-editor').val(), {'gfm':true, 'breaks':true})); var editor_value = $('#new-desc-editor').val();
$(event.target.hash).html(
md.render(editor_value)
);
} }
}); });
$("#solve-attempts-checkbox").change(function() { $("#solve-attempts-checkbox").change(function() {

View File

@ -23,7 +23,3 @@ $(".input-field").bind({
} }
} }
}); });
var content = $('.chal-desc').text();
var decoded = $('<textarea/>').html(content).val()
$('.chal-desc').html(marked(content, {'gfm':true, 'breaks':true}));

View File

@ -25,7 +25,7 @@
<span class='badge badge-info chal-tag'>{{tag}}</span> <span class='badge badge-info chal-tag'>{{tag}}</span>
{% endfor %} {% endfor %}
</div> </div>
<span class="chal-desc">{{ desc }}</span> <span class="chal-desc">{{ description | safe }}</span>
<div class="chal-hints hint-row row"> <div class="chal-hints hint-row row">
{% for hint in hints %} {% for hint in hints %}
<div class='col-md-12 hint-button-wrapper text-center mb-3'> <div class='col-md-12 hint-button-wrapper text-center mb-3'>

View File

@ -18,37 +18,31 @@ $('#limit_max_attempts').change(function() {
// Markdown Preview // Markdown Preview
$('#desc-edit').on('shown.bs.tab', function (event) { $('#desc-edit').on('shown.bs.tab', function (event) {
var md = window.markdownit({
html: true,
});
if (event.target.hash == '#desc-preview'){ if (event.target.hash == '#desc-preview'){
$(event.target.hash).html(marked($('#desc-editor').val(), {'gfm':true, 'breaks':true})) var editor_value = $('#desc-editor').val();
$(event.target.hash).html(
md.render(editor_value)
);
} }
}); });
$('#new-desc-edit').on('shown.bs.tab', function (event) { $('#new-desc-edit').on('shown.bs.tab', function (event) {
var md = window.markdownit({
html: true,
});
if (event.target.hash == '#new-desc-preview'){ if (event.target.hash == '#new-desc-preview'){
$(event.target.hash).html(marked($('#new-desc-editor').val(), {'gfm':true, 'breaks':true})) var editor_value = $('#new-desc-editor').val();
$(event.target.hash).html(
md.render(editor_value)
);
} }
}); });
function loadchal(id, update) { function loadchal(id, update) {
$.get(script_root + '/admin/chal/' + id, function(obj){ $.get(script_root + '/admin/chal/' + id, function(obj){
$('#desc-write-link').click(); // Switch to Write tab $('#desc-write-link').click(); // Switch to Write tab
$('.chal-title').text(obj.name);
$('.chal-name').val(obj.name);
$('.chal-desc-editor').val(obj.description);
$('.chal-value').val(obj.value);
if (parseInt(obj.max_attempts) > 0){
$('.chal-attempts').val(obj.max_attempts);
$('#limit_max_attempts').prop('checked', true);
$('#chal-attempts-group').show();
}
$('.chal-category').val(obj.category);
$('.chal-id').val(obj.id);
$('.chal-hidden').prop('checked', false);
if (obj.hidden) {
$('.chal-hidden').prop('checked', true);
}
//$('#update-challenge .chal-delete').attr({
// 'href': '/admin/chal/close/' + (id + 1)
//})
if (typeof update === 'undefined') if (typeof update === 'undefined')
$('#update-challenge').modal(); $('#update-challenge').modal();
}); });

View File

@ -5,7 +5,7 @@
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">
<h3 class="chal-title text-center"></h3> <h3 class="chal-title text-center">{{ name }}</h3>
</div> </div>
</div> </div>
</div> </div>
@ -21,13 +21,13 @@
<label for="name">Name <label for="name">Name
<i class="far fa-question-circle text-muted cursor-help" data-toggle="tooltip" data-placement="right" title="The name of your challenge"></i> <i class="far fa-question-circle text-muted cursor-help" data-toggle="tooltip" data-placement="right" title="The name of your challenge"></i>
</label> </label>
<input type="text" class="form-control chal-name" name="name" placeholder="Enter challenge name"> <input type="text" class="form-control chal-name" name="name" placeholder="Enter challenge name" value="{{ name }}">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="category">Category <label for="category">Category
<i class="far fa-question-circle text-muted cursor-help" data-toggle="tooltip" data-placement="right" title="The category of your challenge"></i> <i class="far fa-question-circle text-muted cursor-help" data-toggle="tooltip" data-placement="right" title="The category of your challenge"></i>
</label> </label>
<input type="text" class="form-control chal-category" name="category" placeholder="Enter challenge category"> <input type="text" class="form-control chal-category" name="category" placeholder="Enter challenge category" value="{{ category }}">
</div> </div>
<ul class="nav nav-tabs" role="tablist" id="desc-edit"> <ul class="nav nav-tabs" role="tablist" id="desc-edit">
@ -48,7 +48,7 @@
<label for="message-text" class="control-label">Message: <label for="message-text" class="control-label">Message:
<i class="far fa-question-circle text-muted cursor-help" data-toggle="tooltip" data-placement="right" title="Use this to give a brief introduction to your challenge. The description supports HTML and Markdown."></i> <i class="far fa-question-circle text-muted cursor-help" data-toggle="tooltip" data-placement="right" title="Use this to give a brief introduction to your challenge. The description supports HTML and Markdown."></i>
</label> </label>
<textarea id="desc-editor" class="form-control chal-desc-editor" name="description" rows="10"></textarea> <textarea id="desc-editor" class="form-control chal-desc-editor" name="description" rows="10">{{ description }}</textarea>
</div> </div>
</div> </div>
<div role="tabpanel" class="tab-pane content" id="desc-preview" style="height:214px; overflow-y: scroll;"> <div role="tabpanel" class="tab-pane content" id="desc-preview" style="height:214px; overflow-y: scroll;">
@ -59,25 +59,26 @@
<label for="value">Value <label for="value">Value
<i class="far fa-question-circle text-muted cursor-help" data-toggle="tooltip" data-placement="right" title="This is how many points teams will receive once they solve this challenge."></i> <i class="far fa-question-circle text-muted cursor-help" data-toggle="tooltip" data-placement="right" title="This is how many points teams will receive once they solve this challenge."></i>
</label> </label>
<input type="number" class="form-control chal-value" name="value" placeholder="Enter value" required> <input type="number" class="form-control chal-value" name="value" placeholder="Enter value" value="{{ value }}" required>
</div> </div>
<div class="checkbox"> <div class="checkbox">
<label> <label>
<input class="chal-attempts-checkbox" id="limit_max_attempts" name="limit_max_attempts" type="checkbox"> <input class="chal-attempts-checkbox" id="limit_max_attempts" name="limit_max_attempts" type="checkbox" {% if max_attempts %}checked{% endif %}>
Limit challenge attempts Limit challenge attempts
</label> </label>
</div> </div>
<div class="form-group" id="chal-attempts-group" style="display:none;"> <div class="form-group" id="chal-attempts-group" {% if not max_attempts %}style="display:none;"{% endif %}>
<label for="value">Max Attempts</label> <label for="value">Max Attempts</label>
<input type="number" class="form-control chal-attempts" id="chal-attempts-input" name="max_attempts" placeholder="Enter value"> <input type="number" class="form-control chal-attempts" id="chal-attempts-input" name="max_attempts" placeholder="Enter value" value="{{ max_attempts }}">
</div> </div>
<input class="chal-id" type='hidden' name='id' placeholder='ID'> <input class="chal-id" type='hidden' name='id' placeholder='ID' value="{{ id }}">
<div class="checkbox"> <div class="checkbox">
<label> <label>
<input class="chal-hidden" name="hidden" type="checkbox"> <input class="chal-hidden" name="hidden" type="checkbox"
{% if hidden %}checked{% endif %}>
Hidden Hidden
</label> </label>
</div> </div>

View File

@ -5,9 +5,15 @@ function load_chal_template(id, success_cb){
var obj = $.grep(challenges['game'], function (e) { var obj = $.grep(challenges['game'], function (e) {
return e.id == id; return e.id == id;
})[0]; })[0];
$.get(script_root + "/admin/chal/" + id, function (challenge_data) {
$.get(script_root + obj.type_data.templates.update, function (template_data) { $.get(script_root + obj.type_data.templates.update, function (template_data) {
var template = nunjucks.compile(template_data); var template = nunjucks.compile(template_data);
$("#update-modals-entry-div").html(template.render({'nonce':$('#nonce').val(), 'script_root':script_root}));
challenge_data['nonce'] = $('#nonce').val();
challenge_data['script_root'] = script_root;
$("#update-modals-entry-div").html(template.render(challenge_data));
$.ajax({ $.ajax({
url: script_root + obj.type_data.scripts.update, url: script_root + obj.type_data.scripts.update,
dataType: "script", dataType: "script",
@ -15,69 +21,34 @@ function load_chal_template(id, success_cb){
cache: false, cache: false,
}); });
}); });
}
function get_challenge(id){
var obj = $.grep(challenges['game'], function (e) {
return e.id == id;
})[0];
return obj;
}
function load_challenge_preview(id){
loadchal(id, function(){
var chal = get_challenge(id);
var modal_template = chal.type_data.templates.modal;
var modal_script = chal.type_data.scripts.modal;
render_challenge_preview(chal, modal_template, modal_script);
}); });
} }
function render_challenge_preview(chal, modal_template, modal_script){ function load_challenge_preview(id){
render_challenge_preview(id);
}
function render_challenge_preview(chal_id){
var preview_window = $('#challenge-preview'); var preview_window = $('#challenge-preview');
$.get(script_root + modal_template, function (template_data) { var md = window.markdownit({
html: true,
});
$.get(script_root + "/admin/chal/" + chal_id, function(challenge_data){
$.get(script_root + challenge_data.type_data.templates.modal, function (template_data) {
preview_window.empty(); preview_window.empty();
var template = nunjucks.compile(template_data); var template = nunjucks.compile(template_data);
var data = {
id: chal.id,
name: chal.name,
value: chal.value,
tags: chal.tags,
desc: chal.description,
files: chal.files,
hints: chal.hints,
script_root: script_root
};
var challenge = template.render(data); challenge_data['description'] = md.render(challenge_data['description']);
challenge_data['script_root'] = script_root;
var challenge = template.render(challenge_data);
preview_window.append(challenge); preview_window.append(challenge);
$.getScript(script_root + modal_script, function () { $.getScript(script_root + challenge_data.type_data.scripts.modal, function () {
preview_window.modal(); preview_window.modal();
}); });
}); });
}
function loadchal(chalid, cb){
$.get(script_root + "/admin/chal/"+chalid, {
}, function (data) {
var categories = [];
var challenge = $.parseJSON(JSON.stringify(data));
for (var i = challenges['game'].length - 1; i >= 0; i--) {
if (challenges['game'][i]['id'] == challenge.id) {
challenges['game'][i] = challenge
}
}
if (cb) {
cb();
}
}); });
} }
@ -110,6 +81,9 @@ loadchals(function(){
}); });
function loadhint(hintid) { function loadhint(hintid) {
var md = window.markdownit({
html: true,
});
ezq({ ezq({
title: "Unlock Hint?", title: "Unlock Hint?",
body: "Are you sure you want to open this hint?", body: "Are you sure you want to open this hint?",
@ -124,7 +98,7 @@ function loadhint(hintid) {
} else { } else {
ezal({ ezal({
title: "Hint", title: "Hint",
body: marked(data.hint, {'gfm': true, 'breaks': true}), body: md.render(data.hint),
button: "Got it!" button: "Got it!"
}); });
} }

View File

@ -87,11 +87,14 @@ $(document).ready(function () {
$('a[data-toggle="tab"]').on('shown.bs.tab', function (e) { $('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
var md = window.markdownit({
html: true,
});
var target = $(e.target).attr("href"); var target = $(e.target).attr("href");
if (target == '#hint-preview') { if (target == '#hint-preview') {
var obj = $('#hint-modal-hint'); var obj = $('#hint-modal-hint');
var data = marked(obj.val()); var data = md.render(obj.val());
$('#hint-preview').html(data, {'gfm': true, 'breaks': true}); $('#hint-preview').html(data);
} }
}); });

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -113,7 +113,7 @@
</footer> </footer>
<script src="{{ request.script_root }}/themes/admin/static/js/vendor/jquery.min.js"></script> <script src="{{ request.script_root }}/themes/admin/static/js/vendor/jquery.min.js"></script>
<script src="{{ request.script_root }}/themes/admin/static/js/vendor/marked.min.js"></script> <script src="{{ request.script_root }}/themes/admin/static/js/vendor/markdown-it.min.js"></script>
<script src="{{ request.script_root }}/themes/admin/static/js/vendor/bootstrap.bundle.min.js"></script> <script src="{{ request.script_root }}/themes/admin/static/js/vendor/bootstrap.bundle.min.js"></script>
<script src="{{ request.script_root }}/themes/admin/static/js/main.js"></script> <script src="{{ request.script_root }}/themes/admin/static/js/main.js"></script>
<script src="{{ request.script_root }}/themes/admin/static/js/utils.js"></script> <script src="{{ request.script_root }}/themes/admin/static/js/utils.js"></script>

View File

@ -372,13 +372,6 @@
submit_form(); submit_form();
}); });
// Markdown Preview
$('#content-edit').on('shown.bs.tab', function (event) {
if (event.target.hash == '#content-preview') {
$(event.target.hash).html(marked(editor.getValue(), {'gfm': true, 'breaks': true}));
}
});
$('#media-button').click(function () { $('#media-button').click(function () {
$.get(script_root + '/admin/media', function (data) { $.get(script_root + '/admin/media', function (data) {
$('#media-library-list').empty(); $('#media-library-list').empty();

View File

@ -122,7 +122,7 @@
</footer> </footer>
<script src="{{ request.script_root }}/themes/admin/static/js/vendor/jquery.min.js"></script> <script src="{{ request.script_root }}/themes/admin/static/js/vendor/jquery.min.js"></script>
<script src="{{ request.script_root }}/themes/admin/static/js/vendor/marked.min.js"></script> <script src="{{ request.script_root }}/themes/admin/static/js/vendor/markdown-it.min.js"></script>
<script src="{{ request.script_root }}/themes/admin/static/js/vendor/bootstrap.bundle.min.js"></script> <script src="{{ request.script_root }}/themes/admin/static/js/vendor/bootstrap.bundle.min.js"></script>
<script src="{{ request.script_root }}/themes/admin/static/js/main.js"></script> <script src="{{ request.script_root }}/themes/admin/static/js/main.js"></script>
<script src="{{ request.script_root }}/themes/admin/static/js/utils.js"></script> <script src="{{ request.script_root }}/themes/admin/static/js/utils.js"></script>

View File

@ -19,6 +19,7 @@ function loadchalbyname(chalname) {
} }
function updateChalWindow(obj) { function updateChalWindow(obj) {
$.get(script_root + "/chals/" + obj.id, function(challenge_data){
$.get(script_root + obj.template, function (template_data) { $.get(script_root + obj.template, function (template_data) {
$('#chal-window').empty(); $('#chal-window').empty();
var template = nunjucks.compile(template_data); var template = nunjucks.compile(template_data);
@ -26,19 +27,16 @@ function updateChalWindow(obj) {
var solves = obj.solves + solves; var solves = obj.solves + solves;
var nonce = $('#nonce').val(); var nonce = $('#nonce').val();
var wrapper = {
id: obj.id,
name: obj.name,
value: obj.value,
tags: obj.tags,
desc: obj.description,
solves: solves,
files: obj.files,
hints: obj.hints,
script_root: script_root
};
$('#chal-window').append(template.render(wrapper)); var md = window.markdownit({
html: true,
});
challenge_data['description'] = md.render(challenge_data['description']);
challenge_data['script_root'] = script_root;
challenge_data['solves'] = solves;
$('#chal-window').append(template.render(challenge_data));
$.getScript(script_root + obj.script, $.getScript(script_root + obj.script,
function () { function () {
// Handle Solves tab // Handle Solves tab
@ -68,6 +66,7 @@ function updateChalWindow(obj) {
$('#chal-window').modal(); $('#chal-window').modal();
}); });
}); });
});
} }
$("#answer-input").keyup(function(event){ $("#answer-input").keyup(function(event){
@ -282,6 +281,9 @@ function loadchals(cb) {
} }
function loadhint(hintid){ function loadhint(hintid){
var md = window.markdownit({
html: true,
});
ezq({ ezq({
title: "Unlock Hint?", title: "Unlock Hint?",
body: "Are you sure you want to open this hint?", body: "Are you sure you want to open this hint?",
@ -294,9 +296,10 @@ function loadhint(hintid){
button: "Okay" button: "Okay"
}); });
} else { } else {
ezal({ ezal({
title: "Hint", title: "Hint",
body: marked(data.hint, {'gfm': true, 'breaks': true}), body: md.render(data.hint),
button: "Got it!" button: "Got it!"
}); });
} }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -122,7 +122,7 @@
</footer> </footer>
<script src="{{ request.script_root }}/themes/{{ ctf_theme() }}/static/js/vendor/jquery.min.js"></script> <script src="{{ request.script_root }}/themes/{{ ctf_theme() }}/static/js/vendor/jquery.min.js"></script>
<script src="{{ request.script_root }}/themes/{{ ctf_theme() }}/static/js/vendor/marked.min.js"></script> <script src="{{ request.script_root }}/themes/{{ ctf_theme() }}/static/js/vendor/markdown-it.min.js"></script>
<script src="{{ request.script_root }}/themes/{{ ctf_theme() }}/static/js/vendor/bootstrap.bundle.min.js"></script> <script src="{{ request.script_root }}/themes/{{ ctf_theme() }}/static/js/vendor/bootstrap.bundle.min.js"></script>
<script src="{{ request.script_root }}/themes/{{ ctf_theme() }}/static/js/style.js"></script> <script src="{{ request.script_root }}/themes/{{ ctf_theme() }}/static/js/style.js"></script>
<script src="{{ request.script_root }}/themes/{{ ctf_theme() }}/static/js/ezq.js"></script> <script src="{{ request.script_root }}/themes/{{ ctf_theme() }}/static/js/ezq.js"></script>

View File

@ -5,9 +5,8 @@
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">
<h1 class="text-center">403</h1> <h1 class="text-center">Forbidden</h1>
<h2 class="text-center">An authorization error has occured</h2> <h2 class="text-center">{{ error }}</h2>
<h2 class="text-center">Please try again</h2>
</div> </div>
</div> </div>
</div> </div>

View File

@ -58,6 +58,7 @@ class CTFdSerializer(JSONSerializer):
Slightly modified datafreeze serializer so that we can properly Slightly modified datafreeze serializer so that we can properly
export the CTFd database into a zip file. export the CTFd database into a zip file.
""" """
def close(self): def close(self):
for path, result in self.buckets.items(): for path, result in self.buckets.items():
result = self.wrap(result) result = self.wrap(result)
@ -125,19 +126,19 @@ def init_logs(app):
def init_errors(app): def init_errors(app):
@app.errorhandler(404) @app.errorhandler(404)
def page_not_found(error): def page_not_found(error):
return render_template('errors/404.html'), 404 return render_template('errors/404.html', error=error.description), 404
@app.errorhandler(403) @app.errorhandler(403)
def forbidden(error): def forbidden(error):
return render_template('errors/403.html'), 403 return render_template('errors/403.html', error=error.description), 403
@app.errorhandler(500) @app.errorhandler(500)
def general_error(error): def general_error(error):
return render_template('errors/500.html'), 500 return render_template('errors/500.html', error=error.description), 500
@app.errorhandler(502) @app.errorhandler(502)
def gateway_error(error): def gateway_error(error):
return render_template('errors/502.html'), 502 return render_template('errors/502.html', error=error.description), 502
def init_utils(app): def init_utils(app):
@ -287,6 +288,7 @@ def admins_only(f):
return f(*args, **kwargs) return f(*args, **kwargs)
else: else:
return redirect(url_for('auth.login', next=request.path)) return redirect(url_for('auth.login', next=request.path))
return decorated_function return decorated_function
@ -297,6 +299,7 @@ def authed_only(f):
return f(*args, **kwargs) return f(*args, **kwargs)
else: else:
return redirect(url_for('auth.login', next=request.path)) return redirect(url_for('auth.login', next=request.path))
return decorated_function return decorated_function
@ -322,7 +325,9 @@ def ratelimit(method="POST", limit=50, interval=300, key_prefix="rl"):
else: else:
cache.set(key, int(current) + 1, timeout=interval) cache.set(key, int(current) + 1, timeout=interval)
return f(*args, **kwargs) return f(*args, **kwargs)
return decorated_function return decorated_function
return ratelimit_decorator return ratelimit_decorator

76
CTFd/utils/decorators.py Normal file
View File

@ -0,0 +1,76 @@
from flask import request, redirect, url_for, session, abort
from CTFd import utils
import functools
def during_ctf_time_only(f):
'''
Decorator to restrict an endpoint to only be seen during a CTF
:param f:
:return:
'''
@functools.wraps(f)
def during_ctf_time_only_wrapper(*args, **kwargs):
if utils.ctftime() or utils.is_admin():
return f(*args, **kwargs)
else:
if utils.ctf_ended():
if utils.view_after_ctf():
return f(*args, **kwargs)
else:
error = '{} has ended'.format(utils.ctf_name())
abort(403, description=error)
if utils.ctf_started() is False:
error = '{} has not started yet'.format(utils.ctf_name())
abort(403, description=error)
return during_ctf_time_only_wrapper
def require_verified_emails(f):
@functools.wraps(f)
def require_verified_emails_wrapper(*args, **kwargs):
if utils.get_config('verify_emails'):
if utils.authed():
if utils.is_admin() is False and utils.is_verified() is False: # User is not confirmed
return redirect(url_for('auth.confirm_user'))
return f(*args, **kwargs)
return require_verified_emails_wrapper
def viewable_without_authentication(status_code=None):
def viewable_without_authentication_decorator(f):
@functools.wraps(f)
def viewable_without_authentication_wrapper(*args, **kwargs):
if utils.user_can_view_challenges():
return f(*args, **kwargs)
else:
if status_code:
if status_code == 403:
error = "An authorization error has occured"
abort(status_code, description=error)
return redirect(url_for('auth.login', next=request.path))
return viewable_without_authentication_wrapper
return viewable_without_authentication_decorator
def authed_only(f):
@functools.wraps(f)
def authed_only_wrapper(*args, **kwargs):
if session.get('id'):
return f(*args, **kwargs)
else:
return redirect(url_for('auth.login', next=request.path))
return authed_only_wrapper
def admins_only(f):
@functools.wraps(f)
def admins_only_wrapper(*args, **kwargs):
if session.get('admin'):
return f(*args, **kwargs)
else:
return redirect(url_for('auth.login', next=request.path))
return admins_only_wrapper

0
CTFd/utils/helpers.py Normal file
View File

View File

@ -96,6 +96,11 @@ def test_admins_can_access_challenges_before_ctftime():
with freeze_time("2017-10-2"): with freeze_time("2017-10-2"):
client = login_as_user(app, name='admin', password='password') client = login_as_user(app, name='admin', password='password')
r = client.get('/challenges')
assert r.status_code == 200
assert "has not started" in r.get_data(as_text=True)
r = client.get('/chals') r = client.get('/chals')
assert r.status_code == 200 assert r.status_code == 200

View File

@ -276,7 +276,7 @@ def test_ctftime_prevents_accessing_challenges_before_ctf():
chal_id = chal.id chal_id = chal.id
flag = gen_flag(app.db, chal=chal.id, flag=u'flag') flag = gen_flag(app.db, chal=chal.id, flag=u'flag')
with freeze_time("2017-10-3"): with freeze_time("2017-10-3"): # CTF has not started yet.
client = login_as_user(app) client = login_as_user(app)
r = client.get('/chals') r = client.get('/chals')
assert r.status_code == 403 assert r.status_code == 403
@ -288,8 +288,7 @@ def test_ctftime_prevents_accessing_challenges_before_ctf():
} }
r = client.post('/chal/{}'.format(chal_id), data=data) r = client.post('/chal/{}'.format(chal_id), data=data)
data = r.get_data(as_text=True) data = r.get_data(as_text=True)
data = json.loads(data) assert r.status_code == 403
assert data['status'] == -1
solve_count = app.db.session.query(app.db.func.count(Solves.id)).first()[0] solve_count = app.db.session.query(app.db.func.count(Solves.id)).first()[0]
assert solve_count == 0 assert solve_count == 0
destroy_ctfd(app) destroy_ctfd(app)

View File

@ -48,7 +48,8 @@ def test_verify_emails_config():
assert r.status_code == 302 assert r.status_code == 302
r = client.get('/chals') r = client.get('/chals')
assert r.status_code == 403 assert r.location == "http://localhost/confirm"
assert r.status_code == 302
user = Teams.query.filter_by(id=2).first() user = Teams.query.filter_by(id=2).first()
user.verified = True user.verified = True
@ -90,7 +91,8 @@ def test_verify_and_view_unregistered():
assert r.status_code == 302 assert r.status_code == 302
r = client.get('/chals') r = client.get('/chals')
assert r.status_code == 403 assert r.location == "http://localhost/confirm"
assert r.status_code == 302
user = Teams.query.filter_by(id=2).first() user = Teams.query.filter_by(id=2).first()
user.verified = True user.verified = True

View File

@ -45,6 +45,18 @@ def test_viewing_challenges():
destroy_ctfd(app) destroy_ctfd(app)
def test_viewing_challenge():
"""Test that users can see individual challenges"""
app = create_ctfd()
with app.app_context():
register_user(app)
client = login_as_user(app)
gen_challenge(app.db)
r = client.get('/chals/1')
assert json.loads(r.get_data(as_text=True))
destroy_ctfd(app)
def test_chals_solves(): def test_chals_solves():
'''Test that the /chals/solves endpoint works properly''' '''Test that the /chals/solves endpoint works properly'''
app = create_ctfd() app = create_ctfd()

View File

@ -223,7 +223,7 @@ def test_user_get_solves_per_chal():
destroy_ctfd(app) destroy_ctfd(app)
def test_user_get_solves(): def test_user_get_private_solves():
"""Can a registered user load /solves""" """Can a registered user load /solves"""
app = create_ctfd() app = create_ctfd()
with app.app_context(): with app.app_context():
@ -234,7 +234,62 @@ def test_user_get_solves():
destroy_ctfd(app) destroy_ctfd(app)
def test_user_get_team_page(): def test_user_get_public_solves():
"""Can a registered user load /solves/2"""
app = create_ctfd()
with app.app_context():
register_user(app)
client = login_as_user(app)
r = client.get('/solves/2')
assert r.status_code == 200
destroy_ctfd(app)
def test_user_get_another_public_solves():
"""Can a registered user load public solves page of another user"""
app = create_ctfd()
with app.app_context():
register_user(app)
client = login_as_user(app)
r = client.get('/solves/1')
assert r.status_code == 200
destroy_ctfd(app)
def test_user_get_private_fails():
"""Can a registered user load /fails"""
app = create_ctfd()
with app.app_context():
register_user(app)
client = login_as_user(app)
r = client.get('/solves')
assert r.status_code == 200
destroy_ctfd(app)
def test_user_get_public_fails():
"""Can a registered user load /fails/2"""
app = create_ctfd()
with app.app_context():
register_user(app)
client = login_as_user(app)
r = client.get('/fails/2')
assert r.status_code == 200
destroy_ctfd(app)
def test_user_get_another_public_fails():
"""Can a registered user load public fails page of another user"""
app = create_ctfd()
with app.app_context():
register_user(app)
client = login_as_user(app)
r = client.get('/fails/1')
assert r.status_code == 200
destroy_ctfd(app)
def test_user_get_public_team_page():
"""Can a registered user load their public profile (/team/2)""" """Can a registered user load their public profile (/team/2)"""
app = create_ctfd() app = create_ctfd()
with app.app_context(): with app.app_context():
@ -245,6 +300,17 @@ def test_user_get_team_page():
destroy_ctfd(app) destroy_ctfd(app)
def test_user_get_another_public_team_page():
"""Can a registered user load the public profile of another user (/team/1)"""
app = create_ctfd()
with app.app_context():
register_user(app)
client = login_as_user(app)
r = client.get('/team/1')
assert r.status_code == 200
destroy_ctfd(app)
def test_user_get_private_team_page(): def test_user_get_private_team_page():
"""Can a registered user load their private team page (/team)""" """Can a registered user load their private team page (/team)"""
app = create_ctfd() app = create_ctfd()
@ -304,7 +370,7 @@ def test_user_get_logout():
client = login_as_user(app) client = login_as_user(app)
client.get('/logout', follow_redirects=True) client.get('/logout', follow_redirects=True)
r = client.get('/challenges') r = client.get('/challenges')
assert r.location == "http://localhost/login?next=challenges" assert r.location == "http://localhost/login?next=%2Fchallenges"
assert r.status_code == 302 assert r.status_code == 302
destroy_ctfd(app) destroy_ctfd(app)