mirror of https://github.com/JohnHammond/CTFd.git
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 #592selenium-screenshot-testing
parent
d17e599193
commit
51d098080f
|
@ -11,16 +11,22 @@ from CTFd.plugins.keys import get_key_class
|
|||
from CTFd.plugins.challenges import get_chal_class
|
||||
|
||||
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
|
||||
|
||||
challenges = Blueprint('challenges', __name__)
|
||||
|
||||
|
||||
@challenges.route('/hints/<int:hintid>', methods=['GET', 'POST'])
|
||||
@during_ctf_time_only
|
||||
@authed_only
|
||||
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()
|
||||
chal = Challenges.query.filter_by(id=hint.chal).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'])
|
||||
@during_ctf_time_only
|
||||
@require_verified_emails
|
||||
@viewable_without_authentication()
|
||||
def challenges_view():
|
||||
infos = []
|
||||
errors = []
|
||||
start = utils.get_config('start') or 0
|
||||
end = utils.get_config('end') or 0
|
||||
|
||||
if utils.ctf_paused():
|
||||
infos.append('{} is paused'.format(utils.ctf_name()))
|
||||
if not utils.is_admin(): # User is not an admin
|
||||
|
||||
if not utils.ctftime():
|
||||
# It is not CTF time
|
||||
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()))
|
||||
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))
|
||||
else:
|
||||
return redirect(url_for('auth.login', next='challenges'))
|
||||
|
||||
|
||||
@challenges.route('/chals', methods=['GET'])
|
||||
@during_ctf_time_only
|
||||
@require_verified_emails
|
||||
@viewable_without_authentication(status_code=403)
|
||||
def chals():
|
||||
if not utils.is_admin():
|
||||
if not utils.ctftime():
|
||||
if utils.view_after_ctf():
|
||||
pass
|
||||
else:
|
||||
abort(403)
|
||||
|
||||
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,
|
||||
db_chals = Challenges.query.filter(or_(Challenges.hidden != True, Challenges.hidden == None)).order_by(Challenges.value).all()
|
||||
response = {'game': []}
|
||||
for chal in db_chals:
|
||||
tags = [tag.tag for tag in Tags.query.add_columns('tag').filter_by(chal=chal.id).all()]
|
||||
chal_type = get_chal_class(chal.type)
|
||||
response['game'].append({
|
||||
'id': chal.id,
|
||||
'type': chal_type.name,
|
||||
'name': x.name,
|
||||
'value': x.value,
|
||||
'description': x.description,
|
||||
'category': x.category,
|
||||
'files': files,
|
||||
'name': chal.name,
|
||||
'value': chal.value,
|
||||
'category': chal.category,
|
||||
'tags': tags,
|
||||
'hints': hints,
|
||||
'template': chal_type.templates['modal'],
|
||||
'script': chal_type.scripts['modal'],
|
||||
})
|
||||
|
||||
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:
|
||||
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()
|
||||
abort(403)
|
||||
return jsonify(response)
|
||||
|
||||
|
||||
@challenges.route('/chals/solves')
|
||||
@viewable_without_authentication(status_code=403)
|
||||
def solves_per_chal():
|
||||
if not utils.user_can_view_challenges():
|
||||
return redirect(url_for('auth.login', next=request.path))
|
||||
|
||||
chals = Challenges.query\
|
||||
.filter(or_(Challenges.hidden != True, Challenges.hidden == None))\
|
||||
.order_by(Challenges.value)\
|
||||
|
@ -195,21 +197,54 @@ def solves_per_chal():
|
|||
|
||||
|
||||
@challenges.route('/solves')
|
||||
@challenges.route('/solves/<int:teamid>')
|
||||
def solves(teamid=None):
|
||||
@authed_only
|
||||
def solves_private():
|
||||
solves = None
|
||||
awards = None
|
||||
if teamid is None:
|
||||
|
||||
if utils.is_admin():
|
||||
solves = Solves.query.filter_by(teamid=session['id']).all()
|
||||
elif utils.user_can_view_challenges():
|
||||
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:
|
||||
return jsonify({'solves': []})
|
||||
else:
|
||||
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:
|
||||
solves = Solves.query.filter_by(teamid=teamid)
|
||||
awards = Awards.query.filter_by(teamid=teamid)
|
||||
|
@ -241,9 +276,10 @@ def solves(teamid=None):
|
|||
solves = solves.all()
|
||||
awards = awards.all()
|
||||
db.session.close()
|
||||
json = {'solves': []}
|
||||
|
||||
response = {'solves': []}
|
||||
for solve in solves:
|
||||
json['solves'].append({
|
||||
response['solves'].append({
|
||||
'chal': solve.chal.name,
|
||||
'chalid': solve.chalid,
|
||||
'team': solve.teamid,
|
||||
|
@ -253,7 +289,7 @@ def solves(teamid=None):
|
|||
})
|
||||
if awards:
|
||||
for award in awards:
|
||||
json['solves'].append({
|
||||
response['solves'].append({
|
||||
'chal': award.name,
|
||||
'chalid': None,
|
||||
'team': award.teamid,
|
||||
|
@ -261,30 +297,26 @@ def solves(teamid=None):
|
|||
'category': award.category or "Award",
|
||||
'time': utils.unix_time(award.date)
|
||||
})
|
||||
json['solves'].sort(key=lambda k: k['time'])
|
||||
return jsonify(json)
|
||||
|
||||
|
||||
@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)
|
||||
response['solves'].sort(key=lambda k: k['time'])
|
||||
return jsonify(response)
|
||||
|
||||
|
||||
@challenges.route('/fails')
|
||||
@challenges.route('/fails/<int:teamid>')
|
||||
def fails(teamid=None):
|
||||
if teamid is None:
|
||||
@authed_only
|
||||
def fails_private():
|
||||
fails = WrongKeys.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:
|
||||
fails = WrongKeys.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()
|
||||
solves = Solves.query.filter_by(teamid=teamid).count()
|
||||
db.session.close()
|
||||
json = {'fails': str(fails), 'solves': str(solves)}
|
||||
return jsonify(json)
|
||||
response = {
|
||||
'fails': str(fails),
|
||||
'solves': str(solves)
|
||||
}
|
||||
return jsonify(response)
|
||||
|
||||
|
||||
@challenges.route('/chal/<int:chalid>/solves', methods=['GET'])
|
||||
@during_ctf_time_only
|
||||
@viewable_without_authentication(status_code=403)
|
||||
def who_solved(chalid):
|
||||
if not utils.user_can_view_challenges():
|
||||
return redirect(url_for('auth.login', next=request.path))
|
||||
|
||||
json = {'teams': []}
|
||||
response = {'teams': []}
|
||||
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())
|
||||
for solve in solves:
|
||||
json['teams'].append({'id': solve.team.id, 'name': solve.team.name, 'date': solve.date})
|
||||
return jsonify(json)
|
||||
response['teams'].append({'id': solve.team.id, 'name': solve.team.name, 'date': solve.date})
|
||||
return jsonify(response)
|
||||
|
||||
|
||||
@challenges.route('/chal/<int:chalid>', methods=['POST'])
|
||||
@during_ctf_time_only
|
||||
@viewable_without_authentication()
|
||||
def chal(chalid):
|
||||
if utils.ctf_paused():
|
||||
return jsonify({
|
||||
'status': 3,
|
||||
'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():
|
||||
team = Teams.query.filter_by(id=session['id']).first()
|
||||
fails = WrongKeys.query.filter_by(teamid=session['id'], chalid=chalid).count()
|
||||
|
|
|
@ -1,12 +1,24 @@
|
|||
// Markdown Preview
|
||||
$('#desc-edit').on('shown.bs.tab', function (event) {
|
||||
var md = window.markdownit({
|
||||
html: true,
|
||||
});
|
||||
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) {
|
||||
var md = window.markdownit({
|
||||
html: true,
|
||||
});
|
||||
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() {
|
||||
|
|
|
@ -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}));
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
<span class='badge badge-info chal-tag'>{{tag}}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<span class="chal-desc">{{ desc }}</span>
|
||||
<span class="chal-desc">{{ description | safe }}</span>
|
||||
<div class="chal-hints hint-row row">
|
||||
{% for hint in hints %}
|
||||
<div class='col-md-12 hint-button-wrapper text-center mb-3'>
|
||||
|
|
|
@ -18,37 +18,31 @@ $('#limit_max_attempts').change(function() {
|
|||
|
||||
// Markdown Preview
|
||||
$('#desc-edit').on('shown.bs.tab', function (event) {
|
||||
var md = window.markdownit({
|
||||
html: true,
|
||||
});
|
||||
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) {
|
||||
var md = window.markdownit({
|
||||
html: true,
|
||||
});
|
||||
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) {
|
||||
$.get(script_root + '/admin/chal/' + id, function(obj){
|
||||
$('#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')
|
||||
$('#update-challenge').modal();
|
||||
});
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h3 class="chal-title text-center"></h3>
|
||||
<h3 class="chal-title text-center">{{ name }}</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -21,13 +21,13 @@
|
|||
<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>
|
||||
</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 class="form-group">
|
||||
<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>
|
||||
</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>
|
||||
|
||||
<ul class="nav nav-tabs" role="tablist" id="desc-edit">
|
||||
|
@ -48,7 +48,7 @@
|
|||
<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>
|
||||
</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 role="tabpanel" class="tab-pane content" id="desc-preview" style="height:214px; overflow-y: scroll;">
|
||||
|
@ -59,25 +59,26 @@
|
|||
<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>
|
||||
</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 class="checkbox">
|
||||
<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
|
||||
</label>
|
||||
</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>
|
||||
<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>
|
||||
<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">
|
||||
<label>
|
||||
<input class="chal-hidden" name="hidden" type="checkbox">
|
||||
<input class="chal-hidden" name="hidden" type="checkbox"
|
||||
{% if hidden %}checked{% endif %}>
|
||||
Hidden
|
||||
</label>
|
||||
</div>
|
||||
|
|
|
@ -5,9 +5,15 @@ function load_chal_template(id, success_cb){
|
|||
var obj = $.grep(challenges['game'], function (e) {
|
||||
return e.id == id;
|
||||
})[0];
|
||||
$.get(script_root + "/admin/chal/" + id, function (challenge_data) {
|
||||
$.get(script_root + obj.type_data.templates.update, function (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({
|
||||
url: script_root + obj.type_data.scripts.update,
|
||||
dataType: "script",
|
||||
|
@ -15,69 +21,34 @@ function load_chal_template(id, success_cb){
|
|||
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');
|
||||
$.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();
|
||||
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);
|
||||
|
||||
$.getScript(script_root + modal_script, function () {
|
||||
$.getScript(script_root + challenge_data.type_data.scripts.modal, function () {
|
||||
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) {
|
||||
var md = window.markdownit({
|
||||
html: true,
|
||||
});
|
||||
ezq({
|
||||
title: "Unlock Hint?",
|
||||
body: "Are you sure you want to open this hint?",
|
||||
|
@ -124,7 +98,7 @@ function loadhint(hintid) {
|
|||
} else {
|
||||
ezal({
|
||||
title: "Hint",
|
||||
body: marked(data.hint, {'gfm': true, 'breaks': true}),
|
||||
body: md.render(data.hint),
|
||||
button: "Got it!"
|
||||
});
|
||||
}
|
||||
|
|
|
@ -87,11 +87,14 @@ $(document).ready(function () {
|
|||
|
||||
|
||||
$('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
|
||||
var md = window.markdownit({
|
||||
html: true,
|
||||
});
|
||||
var target = $(e.target).attr("href");
|
||||
if (target == '#hint-preview') {
|
||||
var obj = $('#hint-modal-hint');
|
||||
var data = marked(obj.val());
|
||||
$('#hint-preview').html(data, {'gfm': true, 'breaks': true});
|
||||
var data = md.render(obj.val());
|
||||
$('#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
|
@ -113,7 +113,7 @@
|
|||
</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/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/main.js"></script>
|
||||
<script src="{{ request.script_root }}/themes/admin/static/js/utils.js"></script>
|
||||
|
|
|
@ -372,13 +372,6 @@
|
|||
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 () {
|
||||
$.get(script_root + '/admin/media', function (data) {
|
||||
$('#media-library-list').empty();
|
||||
|
|
|
@ -122,7 +122,7 @@
|
|||
</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/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/main.js"></script>
|
||||
<script src="{{ request.script_root }}/themes/admin/static/js/utils.js"></script>
|
||||
|
|
|
@ -19,6 +19,7 @@ function loadchalbyname(chalname) {
|
|||
}
|
||||
|
||||
function updateChalWindow(obj) {
|
||||
$.get(script_root + "/chals/" + obj.id, function(challenge_data){
|
||||
$.get(script_root + obj.template, function (template_data) {
|
||||
$('#chal-window').empty();
|
||||
var template = nunjucks.compile(template_data);
|
||||
|
@ -26,19 +27,16 @@ function updateChalWindow(obj) {
|
|||
var solves = obj.solves + solves;
|
||||
|
||||
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,
|
||||
function () {
|
||||
// Handle Solves tab
|
||||
|
@ -68,6 +66,7 @@ function updateChalWindow(obj) {
|
|||
$('#chal-window').modal();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
$("#answer-input").keyup(function(event){
|
||||
|
@ -282,6 +281,9 @@ function loadchals(cb) {
|
|||
}
|
||||
|
||||
function loadhint(hintid){
|
||||
var md = window.markdownit({
|
||||
html: true,
|
||||
});
|
||||
ezq({
|
||||
title: "Unlock Hint?",
|
||||
body: "Are you sure you want to open this hint?",
|
||||
|
@ -294,9 +296,10 @@ function loadhint(hintid){
|
|||
button: "Okay"
|
||||
});
|
||||
} else {
|
||||
|
||||
ezal({
|
||||
title: "Hint",
|
||||
body: marked(data.hint, {'gfm': true, 'breaks': true}),
|
||||
body: md.render(data.hint),
|
||||
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
|
@ -122,7 +122,7 @@
|
|||
</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/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/style.js"></script>
|
||||
<script src="{{ request.script_root }}/themes/{{ ctf_theme() }}/static/js/ezq.js"></script>
|
||||
|
|
|
@ -5,9 +5,8 @@
|
|||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h1 class="text-center">403</h1>
|
||||
<h2 class="text-center">An authorization error has occured</h2>
|
||||
<h2 class="text-center">Please try again</h2>
|
||||
<h1 class="text-center">Forbidden</h1>
|
||||
<h2 class="text-center">{{ error }}</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -58,6 +58,7 @@ class CTFdSerializer(JSONSerializer):
|
|||
Slightly modified datafreeze serializer so that we can properly
|
||||
export the CTFd database into a zip file.
|
||||
"""
|
||||
|
||||
def close(self):
|
||||
for path, result in self.buckets.items():
|
||||
result = self.wrap(result)
|
||||
|
@ -125,19 +126,19 @@ def init_logs(app):
|
|||
def init_errors(app):
|
||||
@app.errorhandler(404)
|
||||
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)
|
||||
def forbidden(error):
|
||||
return render_template('errors/403.html'), 403
|
||||
return render_template('errors/403.html', error=error.description), 403
|
||||
|
||||
@app.errorhandler(500)
|
||||
def general_error(error):
|
||||
return render_template('errors/500.html'), 500
|
||||
return render_template('errors/500.html', error=error.description), 500
|
||||
|
||||
@app.errorhandler(502)
|
||||
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):
|
||||
|
@ -287,6 +288,7 @@ def admins_only(f):
|
|||
return f(*args, **kwargs)
|
||||
else:
|
||||
return redirect(url_for('auth.login', next=request.path))
|
||||
|
||||
return decorated_function
|
||||
|
||||
|
||||
|
@ -297,6 +299,7 @@ def authed_only(f):
|
|||
return f(*args, **kwargs)
|
||||
else:
|
||||
return redirect(url_for('auth.login', next=request.path))
|
||||
|
||||
return decorated_function
|
||||
|
||||
|
||||
|
@ -322,7 +325,9 @@ def ratelimit(method="POST", limit=50, interval=300, key_prefix="rl"):
|
|||
else:
|
||||
cache.set(key, int(current) + 1, timeout=interval)
|
||||
return f(*args, **kwargs)
|
||||
|
||||
return decorated_function
|
||||
|
||||
return ratelimit_decorator
|
||||
|
||||
|
|
@ -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
|
|
@ -96,6 +96,11 @@ def test_admins_can_access_challenges_before_ctftime():
|
|||
|
||||
with freeze_time("2017-10-2"):
|
||||
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')
|
||||
assert r.status_code == 200
|
||||
|
||||
|
|
|
@ -276,7 +276,7 @@ def test_ctftime_prevents_accessing_challenges_before_ctf():
|
|||
chal_id = chal.id
|
||||
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)
|
||||
r = client.get('/chals')
|
||||
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)
|
||||
data = r.get_data(as_text=True)
|
||||
data = json.loads(data)
|
||||
assert data['status'] == -1
|
||||
assert r.status_code == 403
|
||||
solve_count = app.db.session.query(app.db.func.count(Solves.id)).first()[0]
|
||||
assert solve_count == 0
|
||||
destroy_ctfd(app)
|
||||
|
|
|
@ -48,7 +48,8 @@ def test_verify_emails_config():
|
|||
assert r.status_code == 302
|
||||
|
||||
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.verified = True
|
||||
|
@ -90,7 +91,8 @@ def test_verify_and_view_unregistered():
|
|||
assert r.status_code == 302
|
||||
|
||||
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.verified = True
|
||||
|
|
|
@ -45,6 +45,18 @@ def test_viewing_challenges():
|
|||
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():
|
||||
'''Test that the /chals/solves endpoint works properly'''
|
||||
app = create_ctfd()
|
||||
|
|
|
@ -223,7 +223,7 @@ def test_user_get_solves_per_chal():
|
|||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_user_get_solves():
|
||||
def test_user_get_private_solves():
|
||||
"""Can a registered user load /solves"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
|
@ -234,7 +234,62 @@ def test_user_get_solves():
|
|||
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)"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
|
@ -245,6 +300,17 @@ def test_user_get_team_page():
|
|||
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():
|
||||
"""Can a registered user load their private team page (/team)"""
|
||||
app = create_ctfd()
|
||||
|
@ -304,7 +370,7 @@ def test_user_get_logout():
|
|||
client = login_as_user(app)
|
||||
client.get('/logout', follow_redirects=True)
|
||||
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
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
|
Loading…
Reference in New Issue