* Switching to Flask-Migrate to create tables/database. Adding Hints & Unlocks.
* Adding db.create_all call for sqlite db's (sqlite is not properly handled with alembic yet)
* Python 3 testing works properly with 3.5
* Adding admin side of hints
* Hints are viewable for users
selenium-screenshot-testing
Kevin Chung 2017-03-28 21:17:56 -04:00 committed by GitHub
parent 9a9b775e57
commit f48a0cdacd
20 changed files with 644 additions and 177 deletions

View File

@ -1,7 +1,7 @@
language: python language: python
python: python:
- 2.7 - 2.7
- 3.6 - 3.5
install: install:
- pip install -r development.txt - pip install -r development.txt
script: script:

View File

@ -34,23 +34,26 @@ def create_app(config='CTFd.config.Config'):
if url.drivername == 'postgres': if url.drivername == 'postgres':
url.drivername = 'postgresql' url.drivername = 'postgresql'
## Creates database if the database database does not exist
if not database_exists(url):
create_database(url)
## Register database
db.init_app(app) db.init_app(app)
try: ## Register Flask-Migrate
if not (url.drivername.startswith('sqlite') or database_exists(url)): migrate.init_app(app, db)
create_database(url)
db.create_all() ## This creates tables instead of db.create_all()
except OperationalError: ## Allows migrations to happen properly
db.create_all() migrate_upgrade()
except ProgrammingError: ## Database already exists
pass ## Alembic sqlite support is lacking so we should just create_all anyway
else: if url.drivername.startswith('sqlite'):
db.create_all() db.create_all()
app.db = db app.db = db
migrate.init_app(app, db)
cache.init_app(app) cache.init_app(app)
app.cache = cache app.cache = cache

View File

@ -1,6 +1,6 @@
from flask import current_app as app, render_template, request, redirect, jsonify, url_for, Blueprint from flask import current_app as app, render_template, request, redirect, jsonify, url_for, Blueprint
from CTFd.utils import admins_only, is_admin, cache from CTFd.utils import admins_only, is_admin, cache
from CTFd.models import db, Teams, Solves, Awards, Containers, Challenges, WrongKeys, Keys, Tags, Files, Tracking, Pages, Config, DatabaseError from CTFd.models import db, Teams, Solves, Awards, Containers, Challenges, WrongKeys, Keys, Tags, Files, Tracking, Pages, Config, Hints, Unlocks, DatabaseError
from CTFd.plugins.keys import get_key_class, KEY_CLASSES from CTFd.plugins.keys import get_key_class, KEY_CLASSES
from CTFd.plugins.challenges import get_chal_class, CHALLENGE_CLASSES from CTFd.plugins.challenges import get_chal_class, CHALLENGE_CLASSES
@ -87,6 +87,66 @@ def admin_delete_tags(tagid):
return '1' return '1'
@admin_challenges.route('/admin/hints', defaults={'hintid': None}, methods=['POST', 'GET'])
@admin_challenges.route('/admin/hints/<int:hintid>', methods=['GET', 'POST', 'DELETE'])
@admins_only
def admin_hints(hintid):
if hintid:
hint = Hints.query.filter_by(id=hintid).first_or_404()
if request.method == 'POST':
hint.hint = request.form.get('hint')
hint.chal = int(request.form.get('chal'))
hint.cost = int(request.form.get('cost'))
db.session.commit()
elif request.method == 'DELETE':
db.session.delete(hint)
db.session.commit()
db.session.close()
return ('', 204)
json_data = {
'hint': hint.hint,
'type': hint.type,
'chal': hint.chal,
'cost': hint.cost,
'id': hint.id
}
db.session.close()
return jsonify(json_data)
else:
if request.method == 'GET':
hints = Hints.query.all()
json_data = []
for hint in hints:
json_data.append({
'hint': hint.hint,
'type': hint.type,
'chal': hint.chal,
'cost': hint.cost,
'id': hint.id
})
return jsonify({'results': json_data})
elif request.method == 'POST':
hint = request.form.get('hint')
chalid = int(request.form.get('chal'))
cost = int(request.form.get('cost'))
hint_type = request.form.get('type', 0)
hint = Hints(chal=chalid, hint=hint, cost=cost)
db.session.add(hint)
db.session.commit()
json_data = {
'hint': hint.hint,
'type': hint.type,
'chal': hint.chal,
'cost': hint.cost,
'id': hint.id
}
db.session.close()
return jsonify(json_data)
@admin_challenges.route('/admin/files/<int:chalid>', methods=['GET', 'POST']) @admin_challenges.route('/admin/files/<int:chalid>', methods=['GET', 'POST'])
@admins_only @admins_only
def admin_files(chalid): def admin_files(chalid):
@ -125,13 +185,34 @@ def admin_get_values(chalid, prop):
chal_keys = Keys.query.filter_by(chal=challenge.id).all() chal_keys = Keys.query.filter_by(chal=challenge.id).all()
json_data = {'keys': []} json_data = {'keys': []}
for x in chal_keys: for x in chal_keys:
json_data['keys'].append({'id': x.id, 'key': x.flag, 'type': x.key_type, 'type_name': get_key_class(x.key_type).name}) json_data['keys'].append({
'id': x.id,
'key': x.flag,
'type': x.key_type,
'type_name': get_key_class(x.key_type).name
})
return jsonify(json_data) return jsonify(json_data)
elif prop == 'tags': elif prop == 'tags':
tags = Tags.query.filter_by(chal=chalid).all() tags = Tags.query.filter_by(chal=chalid).all()
json_data = {'tags': []} json_data = {'tags': []}
for x in tags: for x in tags:
json_data['tags'].append({'id': x.id, 'chal': x.chal, 'tag': x.tag}) json_data['tags'].append({
'id': x.id,
'chal': x.chal,
'tag': x.tag
})
return jsonify(json_data)
elif prop == 'hints':
hints = Hints.query.filter_by(chal=chalid)
json_data = {'hints': []}
for hint in hints:
json_data['hints'].append({
'hint': hint.hint,
'type': hint.type,
'chal': hint.chal,
'cost': hint.cost,
'id': hint.id
})
return jsonify(json_data) return jsonify(json_data)

View File

@ -6,7 +6,7 @@ import time
from flask import render_template, request, redirect, jsonify, url_for, session, Blueprint from flask import render_template, request, redirect, jsonify, url_for, session, Blueprint
from sqlalchemy.sql import or_ from sqlalchemy.sql import or_
from CTFd.models import db, Challenges, Files, Solves, WrongKeys, Keys, Tags, Teams, Awards from CTFd.models import db, Challenges, Files, Solves, WrongKeys, Keys, Tags, Teams, Awards, Hints, Unlocks
from CTFd.plugins.keys import get_key_class from CTFd.plugins.keys import get_key_class
from CTFd.plugins.challenges import get_chal_class from CTFd.plugins.challenges import get_chal_class
@ -14,6 +14,49 @@ from CTFd import utils
challenges = Blueprint('challenges', __name__) challenges = Blueprint('challenges', __name__)
@challenges.route('/hints/<int:hintid>', methods=['GET', 'POST'])
def hints_view(hintid):
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()
if request.method == 'GET':
if unlock:
return jsonify({
'hint': hint.hint,
'chal': hint.chal,
'cost': hint.cost
})
else:
return jsonify({
'chal': hint.chal,
'cost': hint.cost
})
elif request.method == 'POST':
if not unlock:
team = Teams.query.filter_by(id=session['id']).first()
if team.score() < hint.cost:
return jsonify({'errors': 'Not enough points'})
unlock = Unlocks(model='hints', teamid=session['id'], itemid=hint.id)
award = Awards(teamid=session['id'], name='Hint for {}'.format(chal.name), value=(-hint.cost))
db.session.add(unlock)
db.session.add(award)
db.session.commit()
json_data = {
'hint': hint.hint,
'chal': hint.chal,
'cost': hint.cost
}
db.session.close()
return jsonify(json_data)
else:
json_data = {
'hint': hint.hint,
'chal': hint.chal,
'cost': hint.cost
}
db.session.close()
return jsonify(json_data)
@challenges.route('/challenges', methods=['GET']) @challenges.route('/challenges', methods=['GET'])
def challenges_view(): def challenges_view():
@ -57,6 +100,14 @@ def chals():
for x in chals: for x in chals:
tags = [tag.tag for tag in Tags.query.add_columns('tag').filter_by(chal=x.id).all()] 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()] 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=session['id'])])
hints = []
for hint in Hints.query.filter_by(chal=x.id).all():
if hint.id in unlocked_hints:
hints.append({'id':hint.id, 'cost':hint.cost, 'hint':hint.hint})
else:
hints.append({'id':hint.id, 'cost':hint.cost})
# hints = [{'id':hint.id, 'cost':hint.cost} for hint in Hints.query.filter_by(chal=x.id).all()]
chal_type = get_chal_class(x.type) chal_type = get_chal_class(x.type)
json['game'].append({ json['game'].append({
'id': x.id, 'id': x.id,
@ -66,7 +117,8 @@ def chals():
'description': x.description, 'description': x.description,
'category': x.category, 'category': x.category,
'files': files, 'files': files,
'tags': tags 'tags': tags,
'hints': hints
}) })
db.session.close() db.session.close()

View File

@ -32,7 +32,7 @@ class Config(object):
http://flask-sqlalchemy.pocoo.org/2.1/config/#configuration-keys http://flask-sqlalchemy.pocoo.org/2.1/config/#configuration-keys
''' '''
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or 'sqlite:///ctfd.db' SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or 'sqlite:///{}/ctfd.db'.format(os.path.dirname(__file__))
''' '''

View File

@ -76,6 +76,23 @@ class Challenges(db.Model):
return '<chal %r>' % self.name return '<chal %r>' % self.name
class Hints(db.Model):
id = db.Column(db.Integer, primary_key=True)
type = db.Column(db.Integer, default=0)
chal = db.Column(db.Integer, db.ForeignKey('challenges.id'))
hint = db.Column(db.Text)
cost = db.Column(db.Integer, default=0)
def __init__(self, chal, hint, cost=0, type=0):
self.chal = chal
self.hint = hint
self.cost = cost
self.type = type
def __repr__(self):
return '<hint %r>' % self.hint
class Awards(db.Model): class Awards(db.Model):
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
teamid = db.Column(db.Integer, db.ForeignKey('teams.id')) teamid = db.Column(db.Integer, db.ForeignKey('teams.id'))
@ -244,6 +261,22 @@ class WrongKeys(db.Model):
return '<wrong %r>' % self.flag return '<wrong %r>' % self.flag
class Unlocks(db.Model):
id = db.Column(db.Integer, primary_key=True)
teamid = db.Column(db.Integer, db.ForeignKey('teams.id'))
date = db.Column(db.DateTime, default=datetime.datetime.utcnow)
itemid = db.Column(db.Integer)
model = db.Column(db.String(32))
def __init__(self, model, teamid, itemid):
self.model = model
self.teamid = teamid
self.itemid = itemid
def __repr__(self):
return '<unlock %r>' % self.teamid
class Tracking(db.Model): class Tracking(db.Model):
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
ip = db.Column(db.BigInteger) ip = db.Column(db.BigInteger)

View File

@ -38,6 +38,25 @@ function load_edit_key_modal(key_id, key_type_name) {
}); });
} }
function load_hint_modal(method, hintid){
$('#hint-modal-hint').val('');
$('#hint-modal-cost').val('');
if (method == 'create'){
$('#hint-modal-submit').attr('action', '/admin/hints');
$('#hint-modal-title').text('Create Hint');
$("#hint-modal").modal();
} else if (method == 'update'){
$.get(script_root + '/admin/hints/' + hintid, function(data){
$('#hint-modal-submit').attr('action', '/admin/hints/' + hintid);
$('#hint-modal-hint').val(data.hint);
$('#hint-modal-cost').val(data.cost);
$('#hint-modal-title').text('Update Hint');
$("#hint-modal-button").text('Update Hint');
$("#hint-modal").modal();
});
}
}
function loadchal(id, update) { function loadchal(id, update) {
// $('#chal *').show() // $('#chal *').show()
// $('#chal > h1').hide() // $('#chal > h1').hide()
@ -169,6 +188,44 @@ function deletetag(tagid){
$.post(script_root + '/admin/tags/'+tagid+'/delete', {'nonce': $('#nonce').val()}); $.post(script_root + '/admin/tags/'+tagid+'/delete', {'nonce': $('#nonce').val()});
} }
function edithint(hintid){
$.get(script_root + '/admin/hints/' + hintid, function(data){
console.log(data);
})
}
function deletehint(hintid){
$.delete(script_root + '/admin/hints/' + hintid, function(data, textStatus, jqXHR){
if (jqXHR.status == 204){
var chalid = $('.chal-id').val();
loadhints(chalid);
}
});
}
function loadhints(chal){
$.get(script_root + '/admin/chal/{0}/hints'.format(chal), function(data){
var table = $('#hintsboard > tbody');
table.empty();
for (var i = 0; i < data.hints.length; i++) {
var hint = data.hints[i]
var hint_row = "<tr>" +
"<td class='hint-entry'>{0}</td>".format(hint.hint) +
"<td class='hint-cost'>{0}</td>".format(hint.cost) +
"<td class='hint-settings'><span>" +
"<i role='button' class='fa fa-pencil-square-o' onclick=javascript:load_hint_modal('update',{0})></i>".format(hint.id)+
"<i role='button' class='fa fa-times' onclick=javascript:deletehint({0})></i>".format(hint.id)+
"</span></td>" +
"</tr>";
table.append(hint_row);
}
});
}
function deletechal(chalid){ function deletechal(chalid){
$.post(script_root + '/admin/chal/delete', {'nonce':$('#nonce').val(), 'id':chalid}); $.post(script_root + '/admin/chal/delete', {'nonce':$('#nonce').val(), 'id':chalid});
} }
@ -255,6 +312,7 @@ function loadchals(){
$('#challenges button').click(function (e) { $('#challenges button').click(function (e) {
loadchal(this.value); loadchal(this.value);
loadkeys(this.value); loadkeys(this.value);
loadhints(this.value);
loadtags(this.value); loadtags(this.value);
loadfiles(this.value); loadfiles(this.value);
}); });
@ -373,6 +431,25 @@ $('#create-keys-submit').click(function (e) {
create_key(chalid, key_data, key_type); create_key(chalid, key_data, key_type);
}); });
$('#create-hint').click(function(e){
e.preventDefault();
load_hint_modal('create');
});
$('#hint-modal-submit').submit(function (e) {
e.preventDefault();
var params = {}
$(this).serializeArray().map(function(x){
params[x.name] = x.value;
});
$.post(script_root + $(this).attr('action'), params, function(data){
loadhints(params['chal']);
});
$("#hint-modal").modal('hide');
});
$(function(){ $(function(){
loadchals(); loadchals();
}) })

View File

@ -61,3 +61,22 @@ Handlebars.registerHelper('if_eq', function(a, b, opts) {
return opts.inverse(this); return opts.inverse(this);
} }
}); });
// http://stepansuvorov.com/blog/2014/04/jquery-put-and-delete/
jQuery.each(["put", "delete"], function(i, method) {
jQuery[method] = function(url, data, callback, type) {
if (jQuery.isFunction(data)) {
type = type || callback;
callback = data;
data = undefined;
}
return jQuery.ajax({
url: url,
type: method,
dataType: type,
data: data,
success: callback
});
};
});

View File

@ -42,6 +42,7 @@ function updateChalWindow(obj) {
desc: marked(obj.description, {'gfm':true, 'breaks':true}), desc: marked(obj.description, {'gfm':true, 'breaks':true}),
solves: solves, solves: solves,
files: obj.files, files: obj.files,
hints: obj.hints
}; };
$('#chal-window').append(template(wrapper)); $('#chal-window').append(template(wrapper));
@ -226,7 +227,7 @@ function loadchals(refresh) {
var chalid = chalinfo.name.replace(/ /g,"-").hashCode(); var chalid = chalinfo.name.replace(/ /g,"-").hashCode();
var catid = chalinfo.category.replace(/ /g,"-").hashCode(); var catid = chalinfo.category.replace(/ /g,"-").hashCode();
var chalwrap = $("<div id='{0}' class='challenge-wrapper col-md-2'></div>".format(chalid)); var chalwrap = $("<div id='{0}' class='challenge-wrapper col-md-2'></div>".format(chalid));
var chalbutton = $("<button class='challenge-button trigger theme-background hide-text' value='{0}' data-toggle='modal' data-target='#chal-window'></div>".format(chalinfo.id)); var chalbutton = $("<button class='challenge-button trigger theme-background hide-text' value='{0}'></div>".format(chalinfo.id));
var chalheader = $("<h5>{0}</h5>".format(chalinfo.name)); var chalheader = $("<h5>{0}</h5>".format(chalinfo.name));
var chalscore = $("<span>{0}</span>".format(chalinfo.value)); var chalscore = $("<span>{0}</span>".format(chalinfo.value));
chalbutton.append(chalheader); chalbutton.append(chalheader);
@ -254,6 +255,19 @@ function loadchals(refresh) {
}); });
} }
function loadhint(hintid){
if (confirm("Are you sure you want to open this hint?")){
$.post(script_root + "/hints/" + hintid, {'nonce': $('#nonce').val()}, function(data){
if (data.errors){
alert(data.errors);
} else {
$('#hint-modal-body').html(marked(data.hint, {'gfm':true, 'breaks':true}));
$('#hint-modal').modal();
}
});
}
}
$('#submit-key').click(function (e) { $('#submit-key').click(function (e) {
submitkey($('#chal-id').val(), $('#answer-input').val(), $('#nonce').val()) submitkey($('#chal-id').val(), $('#answer-input').val(), $('#nonce').val())
}); });

View File

@ -0,0 +1,63 @@
(function($, window) {
'use strict';
var MultiModal = function(element) {
this.$element = $(element);
this.modalCount = 0;
};
MultiModal.BASE_ZINDEX = 1040;
MultiModal.prototype.show = function(target) {
var that = this;
var $target = $(target);
var modalIndex = that.modalCount++;
$target.css('z-index', MultiModal.BASE_ZINDEX + (modalIndex * 20) + 10);
window.setTimeout(function() {
if(modalIndex > 0)
$('.modal-backdrop').not(':first').addClass('hidden');
that.adjustBackdrop();
});
};
MultiModal.prototype.hidden = function(target) {
this.modalCount--;
if(this.modalCount) {
this.adjustBackdrop();
$('body').addClass('modal-open');
}
};
MultiModal.prototype.adjustBackdrop = function() {
var modalIndex = this.modalCount - 1;
$('.modal-backdrop:first').css('z-index', MultiModal.BASE_ZINDEX + (modalIndex * 20));
};
function Plugin(method, target) {
return this.each(function() {
var $this = $(this);
var data = $this.data('multi-modal-plugin');
if(!data)
$this.data('multi-modal-plugin', (data = new MultiModal(this)));
if(method)
data[method](target);
});
}
$.fn.multiModal = Plugin;
$.fn.multiModal.Constructor = MultiModal;
$(document).on('show.bs.modal', function(e) {
$(document).multiModal('show', e.target);
});
$(document).on('hidden.bs.modal', function(e) {
$(document).multiModal('hidden', e.target);
});
}(jQuery, window));

View File

@ -20,6 +20,29 @@
{{/each}} {{/each}}
</div> </div>
<span class="chal-desc">{{{ desc }}}</span> <span class="chal-desc">{{{ desc }}}</span>
<div class="chal-hints file-row row">
{{#each hints}}
<div class='col-md-12 file-button-wrapper'>
<a onclick="javascript:loadhint({{this.id}})">
{{#if this.hint }}
<label class='challenge-wrapper file-wrapper hide-text'>
View Hint
</label>
{{else}}
{{#if this.cost}}
<label class='challenge-wrapper file-wrapper hide-text'>
Unlock Hint for {{this.cost}} points
</label>
{{else}}
<label class='challenge-wrapper file-wrapper hide-text'>
View Hint
</label>
{{/if}}
{{/if}}
</a>
</div>
{{/each}}
</div>
<div class="chal-files file-row row"> <div class="chal-files file-row row">
{{#each files}} {{#each files}}
<div class='col-md-3 file-button-wrapper'> <div class='col-md-3 file-button-wrapper'>

View File

@ -61,3 +61,22 @@ Handlebars.registerHelper('if_eq', function(a, b, opts) {
return opts.inverse(this); return opts.inverse(this);
} }
}); });
// http://stepansuvorov.com/blog/2014/04/jquery-put-and-delete/
jQuery.each(["put", "delete"], function(i, method) {
jQuery[method] = function(url, data, callback, type) {
if (jQuery.isFunction(data)) {
type = type || callback;
callback = data;
data = undefined;
}
return jQuery.ajax({
url: url,
type: method,
dataType: type,
data: data,
success: callback
});
};
});

View File

@ -28,6 +28,7 @@
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h3>New Challenge</h3> <h3>New Challenge</h3>
</div> </div>
<div class="modal-body"> <div class="modal-body">
@ -162,6 +163,7 @@
<div class="form-group"> <div class="form-group">
<a href="#" data-toggle="modal" data-target="#update-tags" class="btn btn-primary">Tags</a> <a href="#" data-toggle="modal" data-target="#update-tags" class="btn btn-primary">Tags</a>
<a href="#" data-toggle="modal" data-target="#update-files" class="btn btn-primary">Files</a> <a href="#" data-toggle="modal" data-target="#update-files" class="btn btn-primary">Files</a>
<a href="#" data-toggle="modal" data-target="#update-hints" class="btn btn-primary">Hints</a>
<a href="#" data-toggle="modal" data-target="#update-keys" class="btn btn-primary">Keys</a> <a href="#" data-toggle="modal" data-target="#update-keys" class="btn btn-primary">Keys</a>
<a href="#" data-toggle="modal" data-target="#delete-chal" class="btn btn-danger">Delete</a> <a href="#" data-toggle="modal" data-target="#delete-chal" class="btn btn-danger">Delete</a>
</div> </div>
@ -179,6 +181,7 @@
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header text-center"> <div class="modal-header text-center">
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h3>Delete Challenge</h3> <h3>Delete Challenge</h3>
</div> </div>
<div class="modal-body"> <div class="modal-body">
@ -202,6 +205,7 @@
<div class="modal-content"> <div class="modal-content">
<input type="hidden" class="chal-id" name="chal-id"> <input type="hidden" class="chal-id" name="chal-id">
<div class="modal-header text-center"> <div class="modal-header text-center">
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h3>Create Key</h3> <h3>Create Key</h3>
</div> </div>
<div class="modal-body"> <div class="modal-body">
@ -275,6 +279,62 @@
</div> </div>
</div> </div>
<div id="hint-modal" class="modal fade" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header text-center">
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h3 id="hint-modal-title"></h3>
</div>
<div class="modal-body">
<form id="hint-modal-submit" method='POST'>
<input type="hidden" class="chal-id" name="chal">
<div class="form-group">
<textarea id="hint-modal-hint" type="text" class="form-control" name="hint" placeholder="Hint"></textarea>
</div>
<div class="form-group">
<input id="hint-modal-cost" type="number" class="form-control" name="cost" placeholder="Cost">
</div>
<div class="row" style="text-align:center;margin-top:20px">
<input type="hidden" value="{{ nonce }}" name="nonce" id="nonce">
<button class="btn btn-theme btn-outlined" id="hint-modal-button">Add Hint</button>
</div>
</form>
</div>
</div>
</div>
</div>
<div id="update-hints" class="modal fade" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header text-center">
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h3>Hints</h3>
</div>
<div class="modal-body">
<div class="row" style="text-align:center">
<a href="#" id="create-hint" class="btn btn-primary" style="margin-bottom:15px;">New Hint</a>
</div>
<div class='current-hints'>
<table id="hintsboard" class="table table-striped">
<thead>
<tr>
<td class="text-center"><b>Hint</b></td>
<td class="text-center"><b>Cost</b></td>
<td class="text-center"><b>Settings</b></td>
</tr>
</thead>
<tbody class="text-center">
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<div id="update-tags" class="modal fade" tabindex="-1"> <div id="update-tags" class="modal fade" tabindex="-1">
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content"> <div class="modal-content">
@ -305,33 +365,6 @@
</div> </div>
</div> </div>
<div id="email-user" class="modal fade" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header text-center">
<h3>Tags</h3>
</div>
<div class="modal-body">
<input type="text" class="tag-insert" maxlength="80" placeholder="Type tag and press Enter">
<input name='nonce' type='hidden' value="{{ nonce }}">
<input id="tags-chal" name='chal' type='hidden'>
<div id="current-tags">
</div>
<br/>
<div id="chal-tags">
</div>
<br/>
<a href="#" id="submit-tags" class="button">Update</a>
<a class="close-reveal-modal">&#215;</a>
</div>
</div>
</div>
</div>
<div style="text-align:center"> <div style="text-align:center">
<br> <br>
<h1 class="text-center">Challenges</h1> <h1 class="text-center">Challenges</h1>

View File

@ -103,6 +103,19 @@
<input id="nonce" type="hidden" name="nonce" value="{{ nonce }}"> <input id="nonce" type="hidden" name="nonce" value="{{ nonce }}">
<div class="modal fade" id="hint-modal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header text-center">
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h3>Hint</h3>
</div>
<div class="modal-body" id="hint-modal-body">
</div>
</div>
</div>
</div>
<div class="modal fade" id="chal-window" tabindex="-1" role="dialog"> <div class="modal fade" id="chal-window" tabindex="-1" role="dialog">
</div> </div>
{% endif %} {% endif %}
@ -110,6 +123,7 @@
{% block scripts %} {% block scripts %}
<script src="{{ request.script_root }}/static/{{ ctf_theme() }}/js/utils.js"></script> <script src="{{ request.script_root }}/static/{{ ctf_theme() }}/js/utils.js"></script>
<script src="{{ request.script_root }}/static/{{ ctf_theme() }}/js/multi-modal.js"></script>
{% if not errors %}<script src="{{ request.script_root }}/static/{{ ctf_theme() }}/js/chalboard.js"></script>{% endif %} {% if not errors %}<script src="{{ request.script_root }}/static/{{ ctf_theme() }}/js/chalboard.js"></script>{% endif %}
<script src="{{ request.script_root }}/static/{{ ctf_theme() }}/js/style.js"></script> <script src="{{ request.script_root }}/static/{{ ctf_theme() }}/js/style.js"></script>
{% endblock %} {% endblock %}

View File

@ -10,7 +10,8 @@ config = context.config
# Interpret the config file for Python logging. # Interpret the config file for Python logging.
# This line sets up loggers basically. # This line sets up loggers basically.
fileConfig(config.config_file_name) ## http://stackoverflow.com/questions/42427487/using-alembic-config-main-redirects-log-output
# fileConfig(config.config_file_name)
logger = logging.getLogger('alembic.env') logger = logging.getLogger('alembic.env')
# add your model's MetaData object here # add your model's MetaData object here

View File

@ -64,7 +64,11 @@ def upgrade():
## Drop flags from challenges ## Drop flags from challenges
print("Dropping flags column from challenges") print("Dropping flags column from challenges")
op.drop_column('challenges', 'flags') try:
op.drop_column('challenges', 'flags')
except sa.exc.OperationalError:
print("Failed to drop flags. Likely due to SQLite")
print("Finished") print("Finished")
# ### end Alembic commands ### # ### end Alembic commands ###

View File

@ -0,0 +1,46 @@
"""Adding Hints and Unlocks tables
Revision ID: c7225db614c1
Revises: d6514ec92738
Create Date: 2017-03-23 01:31:43.940187
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'c7225db614c1'
down_revision = 'd6514ec92738'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('hints',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('type', sa.Integer(), nullable=True),
sa.Column('chal', sa.Integer(), nullable=True),
sa.Column('hint', sa.Text(), nullable=True),
sa.Column('cost', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['chal'], ['challenges.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('unlocks',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('teamid', sa.Integer(), nullable=True),
sa.Column('date', sa.DateTime(), nullable=True),
sa.Column('itemid', sa.Integer(), nullable=True),
sa.Column('model', sa.String(length=32), nullable=True),
sa.ForeignKeyConstraint(['teamid'], ['teams.id'], ),
sa.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('unlocks')
op.drop_table('hints')
# ### end Alembic commands ###

View File

@ -18,142 +18,127 @@ depends_on = None
def upgrade(): def upgrade():
app = create_app() op.create_table('challenges',
engine = sa.create_engine(app.config.get('SQLALCHEMY_DATABASE_URI')) sa.Column('id', sa.Integer(), nullable=False),
# ### commands auto generated by Alembic - please adjust! ### sa.Column('name', sa.String(length=80), nullable=True),
if not engine.dialect.has_table(engine, 'challenges'): sa.Column('description', sa.Text(), nullable=True),
op.create_table('challenges', sa.Column('value', sa.Integer(), nullable=True),
sa.Column('id', sa.Integer(), nullable=False), sa.Column('category', sa.String(length=80), nullable=True),
sa.Column('name', sa.String(length=80), nullable=True), sa.Column('flags', sa.Text(), nullable=True),
sa.Column('description', sa.Text(), nullable=True), sa.Column('hidden', sa.Boolean(), nullable=True),
sa.Column('value', sa.Integer(), nullable=True), sa.PrimaryKeyConstraint('id')
sa.Column('category', sa.String(length=80), nullable=True), )
sa.Column('flags', sa.Text(), nullable=True),
sa.Column('hidden', sa.Boolean(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
if not engine.dialect.has_table(engine, 'config'): op.create_table('config',
op.create_table('config', sa.Column('id', sa.Integer(), nullable=False),
sa.Column('id', sa.Integer(), nullable=False), sa.Column('key', sa.Text(), nullable=True),
sa.Column('key', sa.Text(), nullable=True), sa.Column('value', sa.Text(), nullable=True),
sa.Column('value', sa.Text(), nullable=True), sa.PrimaryKeyConstraint('id')
sa.PrimaryKeyConstraint('id') )
)
if not engine.dialect.has_table(engine, 'containers'): op.create_table('containers',
op.create_table('containers', sa.Column('id', sa.Integer(), nullable=False),
sa.Column('id', sa.Integer(), nullable=False), sa.Column('name', sa.String(length=80), nullable=True),
sa.Column('name', sa.String(length=80), nullable=True), sa.Column('buildfile', sa.Text(), nullable=True),
sa.Column('buildfile', sa.Text(), nullable=True), sa.PrimaryKeyConstraint('id')
sa.PrimaryKeyConstraint('id') )
)
if not engine.dialect.has_table(engine, 'pages'): op.create_table('pages',
op.create_table('pages', sa.Column('id', sa.Integer(), nullable=False),
sa.Column('id', sa.Integer(), nullable=False), sa.Column('route', sa.String(length=80), nullable=True),
sa.Column('route', sa.String(length=80), nullable=True), sa.Column('html', sa.Text(), nullable=True),
sa.Column('html', sa.Text(), nullable=True), sa.PrimaryKeyConstraint('id'),
sa.PrimaryKeyConstraint('id'), sa.UniqueConstraint('route')
sa.UniqueConstraint('route') )
)
if not engine.dialect.has_table(engine, 'teams'): op.create_table('teams',
op.create_table('teams', sa.Column('id', sa.Integer(), nullable=False),
sa.Column('id', sa.Integer(), nullable=False), sa.Column('name', sa.String(length=128), nullable=True),
sa.Column('name', sa.String(length=128), nullable=True), sa.Column('email', sa.String(length=128), nullable=True),
sa.Column('email', sa.String(length=128), nullable=True), sa.Column('password', sa.String(length=128), nullable=True),
sa.Column('password', sa.String(length=128), nullable=True), sa.Column('website', sa.String(length=128), nullable=True),
sa.Column('website', sa.String(length=128), nullable=True), sa.Column('affiliation', sa.String(length=128), nullable=True),
sa.Column('affiliation', sa.String(length=128), nullable=True), sa.Column('country', sa.String(length=32), nullable=True),
sa.Column('country', sa.String(length=32), nullable=True), sa.Column('bracket', sa.String(length=32), nullable=True),
sa.Column('bracket', sa.String(length=32), nullable=True), sa.Column('banned', sa.Boolean(), nullable=True),
sa.Column('banned', sa.Boolean(), nullable=True), sa.Column('verified', sa.Boolean(), nullable=True),
sa.Column('verified', sa.Boolean(), nullable=True), sa.Column('admin', sa.Boolean(), nullable=True),
sa.Column('admin', sa.Boolean(), nullable=True), sa.Column('joined', sa.DateTime(), nullable=True),
sa.Column('joined', sa.DateTime(), nullable=True), sa.PrimaryKeyConstraint('id'),
sa.PrimaryKeyConstraint('id'), sa.UniqueConstraint('email'),
sa.UniqueConstraint('email'), sa.UniqueConstraint('name')
sa.UniqueConstraint('name') )
)
if not engine.dialect.has_table(engine, 'awards'): op.create_table('awards',
op.create_table('awards', sa.Column('id', sa.Integer(), nullable=False),
sa.Column('id', sa.Integer(), nullable=False), sa.Column('teamid', sa.Integer(), nullable=True),
sa.Column('teamid', sa.Integer(), nullable=True), sa.Column('name', sa.String(length=80), nullable=True),
sa.Column('name', sa.String(length=80), nullable=True), sa.Column('description', sa.Text(), nullable=True),
sa.Column('description', sa.Text(), nullable=True), sa.Column('date', sa.DateTime(), nullable=True),
sa.Column('date', sa.DateTime(), nullable=True), sa.Column('value', sa.Integer(), nullable=True),
sa.Column('value', sa.Integer(), nullable=True), sa.Column('category', sa.String(length=80), nullable=True),
sa.Column('category', sa.String(length=80), nullable=True), sa.Column('icon', sa.Text(), nullable=True),
sa.Column('icon', sa.Text(), nullable=True), sa.ForeignKeyConstraint(['teamid'], ['teams.id'], ),
sa.ForeignKeyConstraint(['teamid'], ['teams.id'], ), sa.PrimaryKeyConstraint('id')
sa.PrimaryKeyConstraint('id') )
)
if not engine.dialect.has_table(engine, 'files'): op.create_table('files',
op.create_table('files', sa.Column('id', sa.Integer(), nullable=False),
sa.Column('id', sa.Integer(), nullable=False), sa.Column('chal', sa.Integer(), nullable=True),
sa.Column('chal', sa.Integer(), nullable=True), sa.Column('location', sa.Text(), nullable=True),
sa.Column('location', sa.Text(), nullable=True), sa.ForeignKeyConstraint(['chal'], ['challenges.id'], ),
sa.ForeignKeyConstraint(['chal'], ['challenges.id'], ), sa.PrimaryKeyConstraint('id')
sa.PrimaryKeyConstraint('id') )
)
if not engine.dialect.has_table(engine, 'keys'): op.create_table('keys',
op.create_table('keys', sa.Column('id', sa.Integer(), nullable=False),
sa.Column('id', sa.Integer(), nullable=False), sa.Column('chal', sa.Integer(), nullable=True),
sa.Column('chal', sa.Integer(), nullable=True), sa.Column('key_type', sa.Integer(), nullable=True),
sa.Column('key_type', sa.Integer(), nullable=True), sa.Column('flag', sa.Text(), nullable=True),
sa.Column('flag', sa.Text(), nullable=True), sa.ForeignKeyConstraint(['chal'], ['challenges.id'], ),
sa.ForeignKeyConstraint(['chal'], ['challenges.id'], ), sa.PrimaryKeyConstraint('id')
sa.PrimaryKeyConstraint('id') )
)
if not engine.dialect.has_table(engine, 'solves'): op.create_table('solves',
op.create_table('solves', sa.Column('id', sa.Integer(), nullable=False),
sa.Column('id', sa.Integer(), nullable=False), sa.Column('chalid', sa.Integer(), nullable=True),
sa.Column('chalid', sa.Integer(), nullable=True), sa.Column('teamid', sa.Integer(), nullable=True),
sa.Column('teamid', sa.Integer(), nullable=True), sa.Column('ip', sa.Integer(), nullable=True),
sa.Column('ip', sa.Integer(), nullable=True), sa.Column('flag', sa.Text(), nullable=True),
sa.Column('flag', sa.Text(), nullable=True), sa.Column('date', sa.DateTime(), nullable=True),
sa.Column('date', sa.DateTime(), nullable=True), sa.ForeignKeyConstraint(['chalid'], ['challenges.id'], ),
sa.ForeignKeyConstraint(['chalid'], ['challenges.id'], ), sa.ForeignKeyConstraint(['teamid'], ['teams.id'], ),
sa.ForeignKeyConstraint(['teamid'], ['teams.id'], ), sa.PrimaryKeyConstraint('id'),
sa.PrimaryKeyConstraint('id'), sa.UniqueConstraint('chalid', 'teamid')
sa.UniqueConstraint('chalid', 'teamid') )
)
if not engine.dialect.has_table(engine, 'tags'): op.create_table('tags',
op.create_table('tags', sa.Column('id', sa.Integer(), nullable=False),
sa.Column('id', sa.Integer(), nullable=False), sa.Column('chal', sa.Integer(), nullable=True),
sa.Column('chal', sa.Integer(), nullable=True), sa.Column('tag', sa.String(length=80), nullable=True),
sa.Column('tag', sa.String(length=80), nullable=True), sa.ForeignKeyConstraint(['chal'], ['challenges.id'], ),
sa.ForeignKeyConstraint(['chal'], ['challenges.id'], ), sa.PrimaryKeyConstraint('id')
sa.PrimaryKeyConstraint('id') )
)
if not engine.dialect.has_table(engine, 'tracking'): op.create_table('tracking',
op.create_table('tracking', sa.Column('id', sa.Integer(), nullable=False),
sa.Column('id', sa.Integer(), nullable=False), sa.Column('ip', sa.BigInteger(), nullable=True),
sa.Column('ip', sa.BigInteger(), nullable=True), sa.Column('team', sa.Integer(), nullable=True),
sa.Column('team', sa.Integer(), nullable=True), sa.Column('date', sa.DateTime(), nullable=True),
sa.Column('date', sa.DateTime(), nullable=True), sa.ForeignKeyConstraint(['team'], ['teams.id'], ),
sa.ForeignKeyConstraint(['team'], ['teams.id'], ), sa.PrimaryKeyConstraint('id')
sa.PrimaryKeyConstraint('id') )
)
if not engine.dialect.has_table(engine, 'wrong_keys'): op.create_table('wrong_keys',
op.create_table('wrong_keys', sa.Column('id', sa.Integer(), nullable=False),
sa.Column('id', sa.Integer(), nullable=False), sa.Column('chalid', sa.Integer(), nullable=True),
sa.Column('chalid', sa.Integer(), nullable=True), sa.Column('teamid', sa.Integer(), nullable=True),
sa.Column('teamid', sa.Integer(), nullable=True), sa.Column('date', sa.DateTime(), nullable=True),
sa.Column('date', sa.DateTime(), nullable=True), sa.Column('flag', sa.Text(), nullable=True),
sa.Column('flag', sa.Text(), nullable=True), sa.ForeignKeyConstraint(['chalid'], ['challenges.id'], ),
sa.ForeignKeyConstraint(['chalid'], ['challenges.id'], ), sa.ForeignKeyConstraint(['teamid'], ['teams.id'], ),
sa.ForeignKeyConstraint(['teamid'], ['teams.id'], ), sa.PrimaryKeyConstraint('id')
sa.PrimaryKeyConstraint('id') )
)
# ### end Alembic commands ### # ### end Alembic commands ###

View File

@ -1,4 +1,4 @@
from CTFd import create_app from CTFd import create_app
app = create_app() app = create_app()
app.run(debug=True, threaded=True, host="0.0.0.0", port=4000) app.run(debug=True, threaded=True, host="127.0.0.1", port=4000)

View File

@ -203,5 +203,5 @@ def test_viewing_challenges():
client = login_as_user(app) client = login_as_user(app)
gen_challenge(app.db) gen_challenge(app.db)
r = client.get('/chals') r = client.get('/chals')
chals = json.loads(r.data) chals = json.loads(r.get_data(as_text=True))
assert len(chals['game']) == 1 assert len(chals['game']) == 1