Import export (#244)

* Adding dataset and export function

* Removing unnecessary print

* First try at import_ctf

* Adding UI components

* First successful export and import

* Importing configs

* Alerting response for now
This commit is contained in:
Kevin Chung
2017-04-14 02:53:36 -04:00
committed by GitHub
parent 80575e98fe
commit f4d766473d
4 changed files with 326 additions and 4 deletions

View File

@@ -1,13 +1,15 @@
import hashlib import hashlib
import json import json
import os import os
import datetime
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, \
abort, render_template_string abort, render_template_string, send_file
from passlib.hash import bcrypt_sha256 from passlib.hash import bcrypt_sha256
from sqlalchemy.sql import not_ from sqlalchemy.sql import not_
from sqlalchemy.exc import IntegrityError
from CTFd.utils import admins_only, is_admin, cache from CTFd.utils import admins_only, is_admin, cache, export_ctf, import_ctf
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, DatabaseError
from CTFd.scoreboard import get_standings from CTFd.scoreboard import get_standings
from CTFd.plugins.keys import get_key_class, KEY_CLASSES from CTFd.plugins.keys import get_key_class, KEY_CLASSES
@@ -48,6 +50,44 @@ def admin_plugin_config(plugin):
return '1' return '1'
@admin.route('/admin/import', methods=['GET', 'POST'])
@admins_only
def admin_import_ctf():
backup = request.files['backup']
segments = request.form.get('segments')
errors = []
try:
if segments:
import_ctf(backup, segments=segments.split(','))
else:
import_ctf(backup)
except TypeError:
errors.append('The backup file is invalid')
except IntegrityError as e:
errors.append(e.message)
except Exception as e:
errors.append(type(e).__name__)
if errors:
return errors[0], 500
else:
return redirect(url_for('admin.admin_config'))
@admin.route('/admin/export', methods=['GET', 'POST'])
@admins_only
def admin_export_ctf():
segments = request.args.get('segments')
if segments:
backup = export_ctf(segments.split(','))
else:
backup = export_ctf()
ctf_name = utils.ctf_name()
day = datetime.datetime.now().strftime("%Y-%m-%d")
full_name = "{}.{}.zip".format(ctf_name, day)
return send_file(backup, as_attachment=True, attachment_filename=full_name)
@admin.route('/admin/config', methods=['GET', 'POST']) @admin.route('/admin/config', methods=['GET', 'POST'])
@admins_only @admins_only
def admin_config(): def admin_config():

View File

@@ -19,6 +19,9 @@
<li role="presentation"> <li role="presentation">
<a href="#ctftime-section" aria-controls="ctftime-section" role="tab" data-toggle="tab">CTF Time</a> <a href="#ctftime-section" aria-controls="ctftime-section" role="tab" data-toggle="tab">CTF Time</a>
</li> </li>
<li role="presentation">
<a href="#backup-section" aria-controls="backup-section" role="tab" data-toggle="tab">Backup</a>
</li>
</ul> </ul>
<br><br> <br><br>
<button type="submit" id="submit" tabindex="5" class="btn btn-md btn-primary btn-theme btn-outlined pull-left">Update</button> <button type="submit" id="submit" tabindex="5" class="btn btn-md btn-primary btn-theme btn-outlined pull-left">Update</button>
@@ -32,7 +35,7 @@
aria-hidden="true">×</span></button> aria-hidden="true">×</span></button>
</div> </div>
{% endfor %} {% endfor %}
<input name='nonce' type='hidden' value="{{ nonce }}"> <input id="nonce" name='nonce' type='hidden' value="{{ nonce }}">
<div class="tab-content"> <div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="appearance-section"> <div role="tabpanel" class="tab-pane active" id="appearance-section">
@@ -171,7 +174,6 @@
</div> </div>
</div> </div>
<div role="tabpanel" class="tab-pane" id="ctftime-section"> <div role="tabpanel" class="tab-pane" id="ctftime-section">
<div>
<ul class="nav nav-tabs" role="tablist"> <ul class="nav nav-tabs" role="tablist">
<li role="presentation" class="active"> <li role="presentation" class="active">
<a href="#start-date" aria-controls="start-date" role="tab" data-toggle="tab">Start Time</a> <a href="#start-date" aria-controls="start-date" role="tab" data-toggle="tab">Start Time</a>
@@ -383,6 +385,81 @@
</label> </label>
</div> </div>
</div> </div>
<div role="tabpanel" class="tab-pane" id="backup-section">
<ul class="nav nav-tabs" role="tablist">
<li role="presentation" class="active">
<a href="#export-ctf" aria-controls="export-ctf" role="tab" data-toggle="tab">Export</a>
</li>
<li role="presentation">
<a href="#import-ctf" aria-controls="import-ctf" role="tab" data-toggle="tab">Import</a>
</li>
</ul>
<div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="export-ctf">
<div class="row">
<div class="form-group col-xs-8">
<div class="checkbox">
<label>
<input class="export-config" value="challenges" type="checkbox" checked>Challenges
</label>
</div>
<div class="checkbox">
<label>
<input class="export-config" value="teams" type="checkbox" checked>Teams
</label>
</div>
<div class="checkbox">
<label>
<input class="export-config" value="both" type="checkbox" checked>Solves, Wrong Keys, Unlocks
</label>
</div>
<div class="checkbox">
<label>
<input class="export-config" value="metadata" type="checkbox" checked>Configuration
</label>
</div>
<a href="{{ request.script_root }}/admin/export" id="export-button" class="btn btn-default">Export</a>
</div>
</div>
</div>
<div role="tabpanel" class="tab-pane" id="import-ctf">
<div class="row" id="import-div">
<br>
<div class="form-group col-xs-8">
<label for="container-files">Import File
<input type="file" name="backup" id="import-file" accept=".zip">
</label>
</div>
<div class="form-group col-xs-8">
<div class="checkbox">
<label>
<input class="import-config" value="challenges" type="checkbox" checked>Challenges
</label>
</div>
<div class="checkbox">
<label>
<input class="import-config" value="teams" type="checkbox" checked>Teams
</label>
</div>
<div class="checkbox">
<label>
<input class="import-config" value="both" type="checkbox" checked>Solves, Wrong Keys, Unlocks
</label>
</div>
<div class="checkbox">
<label>
<input class="import-config" value="metadata" type="checkbox" checked>Configuration
</label>
</div>
<input id="import-button" type="submit" class="btn btn-default" value="Import">
</div>
</div>
</div>
</div>
</div>
</div> </div>
</div> </div>
</form> </form>
@@ -475,6 +552,53 @@
load_date_values('freeze'); load_date_values('freeze');
}); });
$('#export-button').click(function(e){
e.preventDefault();
var segments = [];
$.each($('.export-config:checked'), function(key, value){
segments.push($(value).val());
});
segments = segments.join(',');
var href = script_root + '/admin/export';
$('#export-button').attr('href', href+'?segments='+segments);
window.location.href = $('#export-button').attr('href');
});
$('#import-button').click(function(e){
e.preventDefault();
var segments = [];
$.each($('.import-config:checked'), function(key, value){
segments.push($(value).val());
});
segments = segments.join(',');
console.log(segments);
var import_file = document.getElementById('import-file').files[0];
var nonce = $('#nonce').val();
var form_data = new FormData();
form_data.append('segments', segments);
form_data.append('backup', import_file);
form_data.append('nonce', nonce);
$.ajax({
url : script_root + '/admin/import',
type : 'POST',
data : form_data,
processData: false,
contentType: false,
statusCode: {
500: function(resp) {
console.log(resp.responseText);
alert(resp.responseText);
}
},
success : function(data) {
window.location.reload()
}
});
});
$(function () { $(function () {
var hash = window.location.hash; var hash = window.location.hash;

View File

@@ -16,6 +16,9 @@ import sys
import tempfile import tempfile
import time import time
import urllib import urllib
import dataset
import zipfile
import io
from flask import current_app as app, request, redirect, url_for, session, render_template, abort from flask import current_app as app, request, redirect, url_for, session, render_template, abort
from flask_caching import Cache from flask_caching import Cache
@@ -633,3 +636,157 @@ def container_ports(name, verbose=False):
return ports return ports
except subprocess.CalledProcessError: except subprocess.CalledProcessError:
return [] return []
def export_ctf(segments=None):
db = dataset.connect(get_config('SQLALCHEMY_DATABASE_URI'))
if segments is None:
segments = ['challenges', 'teams', 'both', 'metadata']
groups = {
'challenges': [
'challenges',
'files',
'tags',
'keys',
'hints',
],
'teams': [
'teams',
'tracking',
'awards',
],
'both': [
'solves',
'wrong_keys',
'unlocks',
],
'metadata': [
'alembic_version',
'config',
'pages',
'containers',
]
}
## Backup database
backup = io.BytesIO()
backup_zip = zipfile.ZipFile(backup, 'w')
for segment in segments:
group = groups[segment]
for item in group:
result = db[item].all()
result_file = io.BytesIO()
dataset.freeze(result, format='json', fileobj=result_file)
result_file.seek(0)
backup_zip.writestr('db/{}.json'.format(item), result_file.read())
## Backup uploads
upload_folder = os.path.join(os.path.normpath(app.root_path), get_config('UPLOAD_FOLDER'))
for root, dirs, files in os.walk(upload_folder):
for file in files:
parent_dir = os.path.basename(root)
backup_zip.write(os.path.join(root, file), arcname=os.path.join('uploads', parent_dir, file))
backup_zip.close()
backup.seek(0)
return backup
def import_ctf(backup, segments=None, erase=False):
side_db = dataset.connect(get_config('SQLALCHEMY_DATABASE_URI'))
if segments is None:
segments = ['challenges', 'teams', 'both', 'metadata']
if not zipfile.is_zipfile(backup):
raise TypeError
backup = zipfile.ZipFile(backup)
groups = {
'challenges': [
'challenges',
'files',
'tags',
'keys',
'hints',
],
'teams': [
'teams',
'tracking',
'awards',
],
'both': [
'solves',
'wrong_keys',
'unlocks',
],
'metadata': [
'alembic_version',
'config',
'pages',
'containers',
]
}
## Need special handling of metadata
if 'metadata' in segments:
meta = groups['metadata']
segments.remove('metadata')
meta.remove('alembic_version')
for item in meta:
table = side_db[item]
path = "db/{}.json".format(item)
data = backup.open(path).read()
## Some JSON files will be empty
if data:
if item == 'config':
saved = json.loads(data)
for entry in saved['results']:
key = entry['key']
value = entry['value']
set_config(key, value)
elif item == 'pages':
saved = json.loads(data)
for entry in saved['results']:
route = entry['route']
html = entry['html']
page = Pages.query.filter_by(route=route).first()
if page:
page.html = html
else:
page = Pages(route, html)
db.session.add(page)
db.session.commit()
elif item == 'containers':
saved = json.loads(data)
for entry in saved['results']:
name = entry['name']
buildfile = entry['buildfile']
container = Containers.query.filter_by(name=name).first()
if container:
container.buildfile = buildfile
else:
container = Containers(name, buildfile)
db.session.add(container)
db.session.commit()
for segment in segments:
group = groups[segment]
for item in group:
table = side_db[item]
path = "db/{}.json".format(item)
data = backup.open(path).read()
if data:
saved = json.loads(data)
for entry in saved['results']:
entry_id = entry.pop('id', None)
table.insert(entry)
else:
continue

View File

@@ -12,3 +12,4 @@ itsdangerous==0.24
requests==2.13.0 requests==2.13.0
PyMySQL==0.7.10 PyMySQL==0.7.10
gunicorn==19.7.0 gunicorn==19.7.0
dataset==0.8.0