Marking 1.0.0 (#196)

* Use <int:xxx> in routes to prevent some errors 500 (#192)

* Use first_or_404() to prevent some errors 500 (#193)

* Add a populating script for awards. (#191)

* Creating upload_file util

* Marking 1.0.0 in __init__ and starting database migrations

* Upgrading some more HTML

* Adding CHANGELOG.md
selenium-screenshot-testing 1.0.0
Kevin Chung 2017-01-24 23:06:16 -05:00 committed by GitHub
parent 01cb189b22
commit 935027c55d
21 changed files with 482 additions and 110 deletions

19
CHANGELOG.md Normal file
View File

@ -0,0 +1,19 @@
1.0.0 / 2017-01-24
==================
**Implemented enhancements:**
- 1.0.0 release! Things work!
- Manage everything from a browser
- Run Containers
- Themes
- Plugins
- Database migrations
**Closed issues:**
- Closed out 94 issues before tagging 1.0.0
**Merged pull requests:**
- Merged 42 pull requests before tagging 1.0.0

View File

@ -1,13 +1,15 @@
import os
from distutils.version import StrictVersion
from flask import Flask
from jinja2 import FileSystemLoader
from sqlalchemy.engine.url import make_url
from sqlalchemy.exc import OperationalError
from sqlalchemy_utils import database_exists, create_database
from utils import get_config, set_config, cache
from utils import get_config, set_config, cache, migrate, migrate_upgrade
__version__ = '1.0.0'
class ThemeLoader(FileSystemLoader):
def get_source(self, environment, template):
@ -45,14 +47,23 @@ def create_app(config='CTFd.config.Config'):
app.db = db
migrate.init_app(app, db)
cache.init_app(app)
app.cache = cache
version = get_config('ctf_version')
if not version: ## Upgrading from an unversioned CTFd
set_config('ctf_version', __version__)
if version and (StrictVersion(version) < StrictVersion(__version__)): ## Upgrading from an older version of CTFd
migrate_upgrade()
set_config('ctf_version', __version__)
if not get_config('ctf_theme'):
set_config('ctf_theme', 'original')
#Session(app)
from CTFd.views import views
from CTFd.challenges import challenges
from CTFd.scoreboard import scoreboard

View File

@ -5,11 +5,10 @@ import os
from flask import current_app as app, render_template, request, redirect, jsonify, url_for, Blueprint
from passlib.hash import bcrypt_sha256
from sqlalchemy.sql import not_
from werkzeug.utils import secure_filename
from CTFd.utils import admins_only, is_admin, unix_time, get_config, \
set_config, sendmail, rmdir, create_image, delete_image, run_image, container_status, container_ports, \
container_stop, container_start, get_themes, cache
container_stop, container_start, get_themes, cache, upload_file
from CTFd.models import db, Teams, Solves, Awards, Containers, Challenges, WrongKeys, Keys, Tags, Files, Tracking, Pages, Config, DatabaseError
from CTFd.scoreboard import get_standings
@ -209,7 +208,7 @@ def admin_pages(route):
@admin.route('/admin/page/<pageroute>/delete', methods=['POST'])
@admins_only
def delete_page(pageroute):
page = Pages.query.filter_by(route=pageroute).first()
page = Pages.query.filter_by(route=pageroute).first_or_404()
db.session.delete(page)
db.session.commit()
db.session.close()
@ -226,7 +225,7 @@ def list_container():
return render_template('admin/containers.html', containers=containers)
@admin.route('/admin/containers/<container_id>/stop', methods=['POST'])
@admin.route('/admin/containers/<int:container_id>/stop', methods=['POST'])
@admins_only
def stop_container(container_id):
container = Containers.query.filter_by(id=container_id).first_or_404()
@ -236,7 +235,7 @@ def stop_container(container_id):
return '0'
@admin.route('/admin/containers/<container_id>/start', methods=['POST'])
@admin.route('/admin/containers/<int:container_id>/start', methods=['POST'])
@admins_only
def run_container(container_id):
container = Containers.query.filter_by(id=container_id).first_or_404()
@ -252,7 +251,7 @@ def run_container(container_id):
return '0'
@admin.route('/admin/containers/<container_id>/delete', methods=['POST'])
@admin.route('/admin/containers/<int:container_id>/delete', methods=['POST'])
@admins_only
def delete_container(container_id):
container = Containers.query.filter_by(id=container_id).first_or_404()
@ -310,19 +309,18 @@ def admin_chals():
return render_template('admin/chals.html')
@admin.route('/admin/keys/<chalid>', methods=['POST', 'GET'])
@admin.route('/admin/keys/<int:chalid>', methods=['POST', 'GET'])
@admins_only
def admin_keys(chalid):
if request.method == 'GET':
chal = Challenges.query.filter_by(id=chalid).first_or_404()
if request.method == 'GET':
json_data = {'keys': []}
flags = json.loads(chal.flags)
for i, x in enumerate(flags):
json_data['keys'].append({'id': i, 'key': x['flag'], 'type': x['type']})
return jsonify(json_data)
elif request.method == 'POST':
chal = Challenges.query.filter_by(id=chalid).first()
newkeys = request.form.getlist('keys[]')
newvals = request.form.getlist('vals[]')
flags = []
@ -338,7 +336,7 @@ def admin_keys(chalid):
return '1'
@admin.route('/admin/tags/<chalid>', methods=['GET', 'POST'])
@admin.route('/admin/tags/<int:chalid>', methods=['GET', 'POST'])
@admins_only
def admin_tags(chalid):
if request.method == 'GET':
@ -358,7 +356,7 @@ def admin_tags(chalid):
return '1'
@admin.route('/admin/tags/<tagid>/delete', methods=['POST'])
@admin.route('/admin/tags/<int:tagid>/delete', methods=['POST'])
@admins_only
def admin_delete_tags(tagid):
if request.method == 'POST':
@ -369,7 +367,7 @@ def admin_delete_tags(tagid):
return '1'
@admin.route('/admin/files/<chalid>', methods=['GET', 'POST'])
@admin.route('/admin/files/<int:chalid>', methods=['GET', 'POST'])
@admins_only
def admin_files(chalid):
if request.method == 'GET':
@ -391,19 +389,7 @@ def admin_files(chalid):
files = request.files.getlist('files[]')
for f in files:
filename = secure_filename(f.filename)
if len(filename) <= 0:
continue
md5hash = hashlib.md5(os.urandom(64)).hexdigest()
if not os.path.exists(os.path.join(os.path.normpath(app.root_path), 'uploads', md5hash)):
os.makedirs(os.path.join(os.path.normpath(app.root_path), 'uploads', md5hash))
f.save(os.path.join(os.path.normpath(app.root_path), 'uploads', md5hash, filename))
db_f = Files(chalid, (md5hash + '/' + filename))
db.session.add(db_f)
upload_file(file=f, chalid=chalid)
db.session.commit()
db.session.close()
@ -411,7 +397,7 @@ def admin_files(chalid):
@admin.route('/admin/teams', defaults={'page': '1'})
@admin.route('/admin/teams/<page>')
@admin.route('/admin/teams/<int:page>')
@admins_only
def admin_teams(page):
page = abs(int(page))
@ -425,10 +411,10 @@ def admin_teams(page):
return render_template('admin/teams.html', teams=teams, pages=pages, curr_page=page)
@admin.route('/admin/team/<teamid>', methods=['GET', 'POST'])
@admin.route('/admin/team/<int:teamid>', methods=['GET', 'POST'])
@admins_only
def admin_team(teamid):
user = Teams.query.filter_by(id=teamid).first()
user = Teams.query.filter_by(id=teamid).first_or_404()
if request.method == 'GET':
solves = Solves.query.filter_by(teamid=teamid).all()
@ -497,7 +483,7 @@ def admin_team(teamid):
return jsonify({'data': ['success']})
@admin.route('/admin/team/<teamid>/mail', methods=['POST'])
@admin.route('/admin/team/<int:teamid>/mail', methods=['POST'])
@admins_only
def email_user(teamid):
message = request.form.get('msg', None)
@ -508,27 +494,27 @@ def email_user(teamid):
return '0'
@admin.route('/admin/team/<teamid>/ban', methods=['POST'])
@admin.route('/admin/team/<int:teamid>/ban', methods=['POST'])
@admins_only
def ban(teamid):
user = Teams.query.filter_by(id=teamid).first()
user = Teams.query.filter_by(id=teamid).first_or_404()
user.banned = True
db.session.commit()
db.session.close()
return redirect(url_for('admin.admin_scoreboard'))
@admin.route('/admin/team/<teamid>/unban', methods=['POST'])
@admin.route('/admin/team/<int:teamid>/unban', methods=['POST'])
@admins_only
def unban(teamid):
user = Teams.query.filter_by(id=teamid).first()
user = Teams.query.filter_by(id=teamid).first_or_404()
user.banned = False
db.session.commit()
db.session.close()
return redirect(url_for('admin.admin_scoreboard'))
@admin.route('/admin/team/<teamid>/delete', methods=['POST'])
@admin.route('/admin/team/<int:teamid>/delete', methods=['POST'])
@admins_only
def delete_team(teamid):
try:
@ -572,7 +558,7 @@ def admin_scoreboard():
return render_template('admin/scoreboard.html', teams=standings)
@admin.route('/admin/teams/<teamid>/awards', methods=['GET'])
@admin.route('/admin/teams/<int:teamid>/awards', methods=['GET'])
@admins_only
def admin_awards(teamid):
awards = Awards.query.filter_by(teamid=teamid).all()
@ -611,18 +597,14 @@ def create_award():
return '0'
@admin.route('/admin/awards/<award_id>/delete', methods=['POST'])
@admin.route('/admin/awards/<int:award_id>/delete', methods=['POST'])
@admins_only
def delete_award(award_id):
try:
award = Awards.query.filter_by(id=award_id).first()
award = Awards.query.filter_by(id=award_id).first_or_404()
db.session.delete(award)
db.session.commit()
db.session.close()
return '1'
except Exception as e:
print(e)
return '0'
@admin.route('/admin/scores')
@ -671,7 +653,7 @@ def admin_solves(teamid="all"):
return jsonify(json_data)
@admin.route('/admin/solves/<teamid>/<chalid>/solve', methods=['POST'])
@admin.route('/admin/solves/<int:teamid>/<int:chalid>/solve', methods=['POST'])
@admins_only
def create_solve(teamid, chalid):
solve = Solves(chalid=chalid, teamid=teamid, ip='127.0.0.1', flag='MARKED_AS_SOLVED_BY_ADMIN')
@ -681,7 +663,7 @@ def create_solve(teamid, chalid):
return '1'
@admin.route('/admin/solves/<keyid>/delete', methods=['POST'])
@admin.route('/admin/solves/<int:keyid>/delete', methods=['POST'])
@admins_only
def delete_solve(keyid):
solve = Solves.query.filter_by(id=keyid).first_or_404()
@ -691,7 +673,7 @@ def delete_solve(keyid):
return '1'
@admin.route('/admin/wrong_keys/<keyid>/delete', methods=['POST'])
@admin.route('/admin/wrong_keys/<int:keyid>/delete', methods=['POST'])
@admins_only
def delete_wrong_key(keyid):
wrong_key = WrongKeys.query.filter_by(id=keyid).first_or_404()
@ -737,9 +719,10 @@ def admin_stats():
least_solved=least_solved)
@admin.route('/admin/wrong_keys/<page>', methods=['GET'])
@admin.route('/admin/wrong_keys', defaults={'page': '1'}, methods=['GET'])
@admin.route('/admin/wrong_keys/<int:page>', methods=['GET'])
@admins_only
def admin_wrong_key(page='1'):
def admin_wrong_key(page):
page = abs(int(page))
results_per_page = 50
page_start = results_per_page * (page - 1)
@ -759,9 +742,10 @@ def admin_wrong_key(page='1'):
return render_template('admin/wrong_keys.html', wrong_keys=wrong_keys, pages=pages, curr_page=page)
@admin.route('/admin/correct_keys/<page>', methods=['GET'])
@admin.route('/admin/correct_keys', defaults={'page': '1'}, methods=['GET'])
@admin.route('/admin/correct_keys/<int:page>', methods=['GET'])
@admins_only
def admin_correct_key(page='1'):
def admin_correct_key(page):
page = abs(int(page))
results_per_page = 50
page_start = results_per_page * (page - 1)
@ -781,9 +765,10 @@ def admin_correct_key(page='1'):
return render_template('admin/correct_keys.html', solves=solves, pages=pages, curr_page=page)
@admin.route('/admin/fails/<teamid>', methods=['GET'])
@admin.route('/admin/fails/all', defaults={'teamid': 'all'}, methods=['GET'])
@admin.route('/admin/fails/<int:teamid>', methods=['GET'])
@admins_only
def admin_fails(teamid='all'):
def admin_fails(teamid):
if teamid == "all":
fails = WrongKeys.query.join(Teams, WrongKeys.teamid == Teams.id).filter(Teams.banned == False).count()
solves = Solves.query.join(Teams, Solves.teamid == Teams.id).filter(Teams.banned == False).count()
@ -816,19 +801,7 @@ def admin_create_chal():
db.session.commit()
for f in files:
filename = secure_filename(f.filename)
if len(filename) <= 0:
continue
md5hash = hashlib.md5(os.urandom(64)).hexdigest()
if not os.path.exists(os.path.join(os.path.normpath(app.root_path), 'uploads', md5hash)):
os.makedirs(os.path.join(os.path.normpath(app.root_path), 'uploads', md5hash))
f.save(os.path.join(os.path.normpath(app.root_path), 'uploads', md5hash, filename))
db_f = Files(chal.id, (md5hash + '/' + filename))
db.session.add(db_f)
upload_file(file=f, chalid=chal.id)
db.session.commit()
db.session.close()
@ -838,8 +811,7 @@ def admin_create_chal():
@admin.route('/admin/chal/delete', methods=['POST'])
@admins_only
def admin_delete_chal():
challenge = Challenges.query.filter_by(id=request.form['id']).first()
if challenge:
challenge = Challenges.query.filter_by(id=request.form['id']).first_or_404()
WrongKeys.query.filter_by(chalid=challenge.id).delete()
Solves.query.filter_by(chalid=challenge.id).delete()
Keys.query.filter_by(chal=challenge.id).delete()
@ -858,7 +830,7 @@ def admin_delete_chal():
@admin.route('/admin/chal/update', methods=['POST'])
@admins_only
def admin_update_chal():
challenge = Challenges.query.filter_by(id=request.form['id']).first()
challenge = Challenges.query.filter_by(id=request.form['id']).first_or_404()
challenge.name = request.form['name']
challenge.description = request.form['desc']
challenge.value = request.form['value']

View File

@ -27,7 +27,7 @@ def confirm_user(data=None):
return render_template('confirm.html', errors=['Your confirmation link seems wrong'])
except:
return render_template('confirm.html', errors=['Your link appears broken, please try again.'])
team = Teams.query.filter_by(email=email).first()
team = Teams.query.filter_by(email=email).first_or_404()
team.verified = True
db.session.commit()
db.session.close()
@ -39,7 +39,7 @@ def confirm_user(data=None):
if not data and request.method == "GET": # User has been directed to the confirm page because his account is not verified
if not authed():
return redirect(url_for('auth.login'))
team = Teams.query.filter_by(id=session['id']).first()
team = Teams.query.filter_by(id=session['id']).first_or_404()
if team.verified:
return redirect(url_for('views.profile'))
else:
@ -60,7 +60,7 @@ def reset_password(data=None):
return render_template('reset_password.html', errors=['Your link has expired'])
except:
return render_template('reset_password.html', errors=['Your link appears broken, please try again.'])
team = Teams.query.filter_by(name=name).first()
team = Teams.query.filter_by(name=name).first_or_404()
team.password = bcrypt_sha256.encrypt(request.form['password'].strip())
db.session.commit()
db.session.close()

View File

@ -79,7 +79,7 @@ def solves_per_chal():
@challenges.route('/solves')
@challenges.route('/solves/<teamid>')
@challenges.route('/solves/<int:teamid>')
def solves(teamid=None):
solves = None
awards = None
@ -131,7 +131,7 @@ def attempts():
return jsonify(json)
@challenges.route('/fails/<teamid>', methods=['GET'])
@challenges.route('/fails/<int:teamid>', methods=['GET'])
def fails(teamid):
fails = WrongKeys.query.filter_by(teamid=teamid).count()
solves = Solves.query.filter_by(teamid=teamid).count()
@ -140,7 +140,7 @@ def fails(teamid):
return jsonify(json)
@challenges.route('/chal/<chalid>/solves', methods=['GET'])
@challenges.route('/chal/<int:chalid>/solves', methods=['GET'])
def who_solved(chalid):
if not user_can_view_challenges():
return redirect(url_for('auth.login', next=request.path))
@ -151,7 +151,7 @@ def who_solved(chalid):
return jsonify(json)
@challenges.route('/chal/<chalid>', methods=['POST'])
@challenges.route('/chal/<int:chalid>', methods=['POST'])
def chal(chalid):
if ctf_ended() and not view_after_ctf():
return redirect(url_for('challenges.challenges_view'))
@ -178,7 +178,7 @@ def chal(chalid):
# Challange not solved yet
if not solves:
chal = Challenges.query.filter_by(id=chalid).first()
chal = Challenges.query.filter_by(id=chalid).first_or_404()
key = unicode(request.form['key'].strip().lower())
keys = json.loads(chal.flags)

View File

@ -52,7 +52,7 @@ def scores():
return jsonify(json)
@scoreboard.route('/top/<count>')
@scoreboard.route('/top/<int:count>')
def topteams(count):
if get_config('view_scoreboard_if_authed') and not authed():
return redirect(url_for('auth.login', next=request.path))

View File

@ -85,7 +85,8 @@
</div>
<div class="form-group">
<label for="exampleInputFile">Upload challenge files</label>
<label>Upload challenge files</label>
<sub class="help-block">Attach multiple files using Control+Click or Cmd+Click.</sub>
<input type="file" name="files[]" multiple="multiple">
</div>
<input type="hidden" value="{{ nonce }}" name="nonce" id="nonce">
@ -220,6 +221,7 @@
<div id="current-files"></div>
<input type="hidden" name="method" value="upload">
<input type="file" name="files[]" multiple="multiple">
<sub class="help-block">Attach multiple files using Control+Click or Cmd+Click.</sub>
<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" type="submit">Update</button>

View File

@ -429,6 +429,18 @@
});
$(function () {
var hash = window.location.hash;
if (hash) {
hash = hash.replace("<>[]'\"", "");
$('ul.nav a[href="' + hash + '"]').tab('show');
}
$('.nav-pills a').click(function (e) {
$(this).tab('show');
window.location.hash = this.hash;
});
var start = $('#start').val();
var end = $('#end').val();
console.log(start);

View File

@ -17,12 +17,14 @@
</div>
<div class="form-group">
<label for="buildfile-editor" class="control-label">Build File</label>
<textarea id="buildfile-editor" class="form-control" name="buildfile" rows="10"></textarea>
<textarea id="buildfile-editor" class="form-control" name="buildfile" rows="10" placeholder="Enter container build file"></textarea>
</div>
<div class="form-group">
<label for="container-files">File input</label>
<label for="container-files">Associated Files
<i class="fa fa-question-circle" title="These files are uploaded alongside your buildfile"></i>
</label>
<input type="file" name="files[]" id="container-files" multiple>
<p class="help-block">These files are uploaded alongside your buildfile</p>
<sub class="help-block">Attach multiple files using Control+Click or Cmd+Click.</sub>
</div>
<input type="hidden" value="{{ nonce }}" name="nonce" id="nonce">
</div>

View File

@ -24,6 +24,7 @@
<div class="row-fluid">
<div class="col-md-12">
<h3>Route: </h3>
<p class="help-block">This is the URL route that your page will be at (e.g. /page)</p>
<input name='nonce' type='hidden' value="{{ nonce }}">
<input class="form-control radius" id="route" type="text" name="route" value="{% if page is defined %}{{ page.route }}{% endif %}" placeholder="Route">
</div>
@ -32,6 +33,7 @@
<div class="row-fluid">
<div class="col-md-12">
<h3>Content: </h3>
<p class="help-block">This is the HTML content of your page</p>
<textarea id="admin-pages-editor" name="html">{% if page is defined %}{{ page.html }}{% endif %}</textarea><br>
<button class="btn btn-theme btn-outlined create-challenge pull-right">
{% if page is defined %}

View File

@ -19,13 +19,16 @@ import urllib
from flask import current_app as app, request, redirect, url_for, session, render_template, abort
from flask_caching import Cache
from flask_migrate import Migrate, upgrade as migrate_upgrade
from itsdangerous import Signer
import six
from six.moves.urllib.parse import urlparse, urljoin
from werkzeug.utils import secure_filename
from CTFd.models import db, WrongKeys, Pages, Config, Tracking, Teams, Containers, ip2long, long2ip
from CTFd.models import db, WrongKeys, Pages, Config, Tracking, Teams, Files, Containers, ip2long, long2ip
cache = Cache()
migrate = Migrate()
def init_logs(app):
@ -297,6 +300,24 @@ def get_themes():
if os.path.isdir(os.path.join(dir, name)) and name != 'admin']
def upload_file(file, chalid):
filename = secure_filename(file.filename)
if len(filename) <= 0:
return False
md5hash = hashlib.md5(os.urandom(64)).hexdigest()
if not os.path.exists(os.path.join(os.path.normpath(app.root_path), 'uploads', md5hash)):
os.makedirs(os.path.join(os.path.normpath(app.root_path), 'uploads', md5hash))
file.save(os.path.join(os.path.normpath(app.root_path), 'uploads', md5hash, filename))
db_f = Files(chalid, (md5hash + '/' + filename))
db.session.add(db_f)
db.session.commit()
return True
@cache.memoize()
def get_config(key):
config = Config.query.filter_by(key=key).first()

View File

@ -105,15 +105,12 @@ def static_html(template):
try:
return render_template('%s.html' % template)
except TemplateNotFound:
page = Pages.query.filter_by(route=template).first()
if page:
page = Pages.query.filter_by(route=template).first_or_404()
return render_template('page.html', content=page.html)
else:
abort(404)
@views.route('/teams', defaults={'page': '1'})
@views.route('/teams/<page>')
@views.route('/teams/<int:page>')
def teams(page):
page = abs(int(page))
results_per_page = 50
@ -130,7 +127,7 @@ def teams(page):
return render_template('teams.html', teams=teams, team_pages=pages, curr_page=page)
@views.route('/team/<teamid>', methods=['GET', 'POST'])
@views.route('/team/<int:teamid>', methods=['GET', 'POST'])
def team(teamid):
if get_config('view_scoreboard_if_authed') and not authed():
return redirect(url_for('auth.login', next=request.path))

13
manage.py Normal file
View File

@ -0,0 +1,13 @@
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_script import Manager
from flask_migrate import Migrate, MigrateCommand
from CTFd import create_app
app = create_app()
manager = Manager(app)
manager.add_command('db', MigrateCommand)
if __name__ == '__main__':
manager.run()

1
migrations/README Executable file
View File

@ -0,0 +1 @@
Generic single-database configuration.

45
migrations/alembic.ini Normal file
View File

@ -0,0 +1,45 @@
# A generic, single database configuration.
[alembic]
# template used to generate migration files
# file_template = %%(rev)s_%%(slug)s
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S

87
migrations/env.py Executable file
View File

@ -0,0 +1,87 @@
from __future__ import with_statement
from alembic import context
from sqlalchemy import engine_from_config, pool
from logging.config import fileConfig
import logging
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
fileConfig(config.config_file_name)
logger = logging.getLogger('alembic.env')
# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
from flask import current_app
config.set_main_option('sqlalchemy.url',
current_app.config.get('SQLALCHEMY_DATABASE_URI'))
target_metadata = current_app.extensions['migrate'].db.metadata
# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
def run_migrations_offline():
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = config.get_main_option("sqlalchemy.url")
context.configure(url=url)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online():
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
# this callback is used to prevent an auto-migration from being generated
# when there are no changes to the schema
# reference: http://alembic.readthedocs.org/en/latest/cookbook.html
def process_revision_directives(context, revision, directives):
if getattr(config.cmd_opts, 'autogenerate', False):
script = directives[0]
if script.upgrade_ops.is_empty():
directives[:] = []
logger.info('No changes in schema detected.')
engine = engine_from_config(config.get_section(config.config_ini_section),
prefix='sqlalchemy.',
poolclass=pool.NullPool)
connection = engine.connect()
context.configure(connection=connection,
target_metadata=target_metadata,
process_revision_directives=process_revision_directives,
**current_app.extensions['migrate'].configure_args)
try:
with context.begin_transaction():
context.run_migrations()
finally:
connection.close()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()

24
migrations/script.py.mako Executable file
View File

@ -0,0 +1,24 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
branch_labels = ${repr(branch_labels)}
depends_on = ${repr(depends_on)}
def upgrade():
${upgrades if upgrades else "pass"}
def downgrade():
${downgrades if downgrades else "pass"}

View File

@ -0,0 +1,148 @@
"""empty message
Revision ID: cb3cfcc47e2f
Revises:
Create Date: 2017-01-17 15:39:42.804290
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'cb3cfcc47e2f'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('challenges',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=80), nullable=True),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('value', sa.Integer(), nullable=True),
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')
)
op.create_table('config',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('key', sa.Text(), nullable=True),
sa.Column('value', sa.Text(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_table('containers',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=80), nullable=True),
sa.Column('buildfile', sa.Text(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_table('pages',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('route', sa.String(length=80), nullable=True),
sa.Column('html', sa.Text(), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('route')
)
op.create_table('teams',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', 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('website', 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('bracket', sa.String(length=32), nullable=True),
sa.Column('banned', sa.Boolean(), nullable=True),
sa.Column('verified', sa.Boolean(), nullable=True),
sa.Column('admin', sa.Boolean(), nullable=True),
sa.Column('joined', sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('email'),
sa.UniqueConstraint('name')
)
op.create_table('awards',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('teamid', sa.Integer(), nullable=True),
sa.Column('name', sa.String(length=80), nullable=True),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('date', sa.DateTime(), nullable=True),
sa.Column('value', sa.Integer(), nullable=True),
sa.Column('category', sa.String(length=80), nullable=True),
sa.Column('icon', sa.Text(), nullable=True),
sa.ForeignKeyConstraint(['teamid'], ['teams.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('files',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('chal', sa.Integer(), nullable=True),
sa.Column('location', sa.Text(), nullable=True),
sa.ForeignKeyConstraint(['chal'], ['challenges.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('keys',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('chal', sa.Integer(), nullable=True),
sa.Column('key_type', sa.Integer(), nullable=True),
sa.Column('flag', sa.Text(), nullable=True),
sa.ForeignKeyConstraint(['chal'], ['challenges.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('solves',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('chalid', sa.Integer(), nullable=True),
sa.Column('teamid', sa.Integer(), nullable=True),
sa.Column('ip', sa.Integer(), nullable=True),
sa.Column('flag', sa.Text(), nullable=True),
sa.Column('date', sa.DateTime(), nullable=True),
sa.ForeignKeyConstraint(['chalid'], ['challenges.id'], ),
sa.ForeignKeyConstraint(['teamid'], ['teams.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('chalid', 'teamid')
)
op.create_table('tags',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('chal', sa.Integer(), nullable=True),
sa.Column('tag', sa.String(length=80), nullable=True),
sa.ForeignKeyConstraint(['chal'], ['challenges.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('tracking',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('ip', sa.BigInteger(), nullable=True),
sa.Column('team', sa.Integer(), nullable=True),
sa.Column('date', sa.DateTime(), nullable=True),
sa.ForeignKeyConstraint(['team'], ['teams.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('wrong_keys',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('chalid', sa.Integer(), nullable=True),
sa.Column('teamid', sa.Integer(), nullable=True),
sa.Column('date', sa.DateTime(), nullable=True),
sa.Column('flag', sa.Text(), nullable=True),
sa.ForeignKeyConstraint(['chalid'], ['challenges.id'], ),
sa.ForeignKeyConstraint(['teamid'], ['teams.id'], ),
sa.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('wrong_keys')
op.drop_table('tracking')
op.drop_table('tags')
op.drop_table('solves')
op.drop_table('keys')
op.drop_table('files')
op.drop_table('awards')
op.drop_table('teams')
op.drop_table('pages')
op.drop_table('containers')
op.drop_table('config')
op.drop_table('challenges')
# ### end Alembic commands ###

View File

@ -6,12 +6,13 @@ import hashlib
import random
from CTFd import create_app
from CTFd.models import Teams, Solves, Challenges, WrongKeys, Keys, Files
from CTFd.models import Teams, Solves, Challenges, WrongKeys, Keys, Files, Awards
app = create_app()
USER_AMOUNT = 50
CHAL_AMOUNT = 20
AWARDS_AMOUNT = 5
categories = [
'Exploitation',
@ -270,6 +271,20 @@ if __name__ == '__main__':
db.session.commit()
# Generating Awards
print("GENERATING AWARDS")
for x in range(USER_AMOUNT):
base_time = datetime.datetime.utcnow() + datetime.timedelta(minutes=-10000)
for _ in range(random.randint(0, AWARDS_AMOUNT)):
award = Awards(x + 1, gen_word(), random.randint(-10, 10))
new_base = random_date(base_time, base_time + datetime.timedelta(minutes=random.randint(30, 60)))
award.date = new_base
base_time = new_base
db.session.add(award)
db.session.commit()
# Generating Wrong Keys
print("GENERATING WRONG KEYS")
for x in range(USER_AMOUNT):

View File

@ -2,6 +2,7 @@ Flask
Flask-SQLAlchemy
Flask-Session
Flask-Caching
Flask-Migrate
SQLAlchemy
sqlalchemy-utils
passlib

View File

@ -173,7 +173,7 @@ def test_user_get_profile():
def test_user_get_logout():
"""Can a registered user can load /logout"""
"""Can a registered user load /logout"""
app = create_ctfd()
with app.app_context():
register_user(app)
@ -185,7 +185,7 @@ def test_user_get_logout():
def test_user_get_reset_password():
"""Can an unregistered user can load /reset_password"""
"""Can an unregistered user load /reset_password"""
app = create_ctfd()
with app.app_context():
register_user(app)