mirror of https://github.com/JohnHammond/CTFd.git
Adding container support
parent
a479b3a7aa
commit
d2778c30ad
|
@ -1,5 +1,5 @@
|
|||
from flask import render_template, request, redirect, abort, jsonify, url_for, session, Blueprint
|
||||
from CTFd.utils import sha512, is_safe_url, authed, admins_only, is_admin, unix_time, unix_time_millis, get_config, set_config, sendmail, rmdir, create_container
|
||||
from CTFd.utils import sha512, is_safe_url, authed, admins_only, is_admin, unix_time, unix_time_millis, get_config, set_config, sendmail, rmdir, create_image, delete_image, run_image, container_status, container_ports, container_stop, container_start
|
||||
from CTFd.models import db, Teams, Solves, Awards, Containers, Challenges, WrongKeys, Keys, Tags, Files, Tracking, Pages, Config, DatabaseError
|
||||
from itsdangerous import TimedSerializer, BadTimeSignature
|
||||
from sqlalchemy.sql import and_, or_, not_
|
||||
|
@ -227,19 +227,58 @@ def delete_page(pageroute):
|
|||
@admins_only
|
||||
def list_container():
|
||||
containers = Containers.query.all()
|
||||
for c in containers:
|
||||
c.status = container_status(c.name)
|
||||
c.ports = ", ".join(container_ports(c.name, verbose=True))
|
||||
return render_template('admin/containers.html', containers=containers)
|
||||
|
||||
|
||||
@admin.route('/admin/containers/<container_id>/stop', methods=['POST'])
|
||||
@admins_only
|
||||
def stop_container(container_id):
|
||||
container = Containers.query.filter_by(id=container_id).first_or_404()
|
||||
if container_stop(container.name):
|
||||
return '1'
|
||||
else:
|
||||
return '0'
|
||||
|
||||
|
||||
@admin.route('/admin/containers/<container_id>/start', methods=['POST'])
|
||||
@admins_only
|
||||
def run_container(container_id):
|
||||
container = Containers.query.filter_by(id=container_id).first_or_404()
|
||||
if container_status(container.name) == 'missing':
|
||||
if run_image(container.name):
|
||||
return '1'
|
||||
else:
|
||||
return '0'
|
||||
else:
|
||||
if container_start(container.name):
|
||||
return '1'
|
||||
else:
|
||||
return '0'
|
||||
|
||||
|
||||
@admin.route('/admin/containers/<container_id>/delete', methods=['POST'])
|
||||
@admins_only
|
||||
def delete_container(container_id):
|
||||
container = Containers.query.filter_by(id=container_id).first_or_404()
|
||||
if delete_image(container.name):
|
||||
db.session.delete(container)
|
||||
db.session.commit()
|
||||
db.session.close()
|
||||
return '1'
|
||||
|
||||
|
||||
@admin.route('/admin/containers/new', methods=['POST'])
|
||||
@admins_only
|
||||
def new_container():
|
||||
name = request.form.get('name')
|
||||
buildfile = request.form.get('buildfile')
|
||||
files = request.files.getlist('files[]')
|
||||
print name
|
||||
print buildfile
|
||||
print files
|
||||
create_container(name=name, buildfile=buildfile, files=files)
|
||||
create_image(name=name, buildfile=buildfile, files=files)
|
||||
run_image(name)
|
||||
return redirect('/admin/containers')
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -67,6 +67,8 @@ def chals_per_solves():
|
|||
@challenges.route('/solves')
|
||||
@challenges.route('/solves/<teamid>')
|
||||
def solves(teamid=None):
|
||||
solves = None
|
||||
awards = None
|
||||
if teamid is None:
|
||||
if is_admin():
|
||||
solves = Solves.query.filter_by(teamid=session['id']).all()
|
||||
|
|
|
@ -35,7 +35,9 @@
|
|||
<li><a href="/admin/pages">Pages</a></li>
|
||||
<li><a href="/admin/teams">Teams</a></li>
|
||||
<li><a href="/admin/scoreboard">Scoreboard</a></li>
|
||||
<li><a href="/admin/containers">Containers</a></li>
|
||||
{% if can_create_container() %}
|
||||
<li><a href="/admin/containers">Containers</a></li>
|
||||
{% endif %}
|
||||
<li><a href="/admin/chals">Challenges</a></li>
|
||||
<li><a href="/admin/statistics">Statistics</a></li>
|
||||
<li><a href="/admin/config">Config</a></li>
|
||||
|
|
|
@ -34,6 +34,32 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div id="confirm" class="modal fade" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2 class="text-center"><span id="confirm-container-title"></span> Container</h2>
|
||||
</div>
|
||||
<div class="modal-body" style="height:110px">
|
||||
<div class="row-fluid">
|
||||
<div class="col-md-12">
|
||||
<form method="POST">
|
||||
<input id="nonce" type="hidden" name="nonce" value="{{ nonce }}">
|
||||
<div class="small-6 small-centered text-center columns">
|
||||
<p>Are you sure you want to <span id="confirm-container-method"></span> <strong id="confirm-container-name"></strong>?</p>
|
||||
<button type="button" data-dismiss="modal" class="btn btn-theme btn-outlined">No</button>
|
||||
<button type="button" id="confirm-container" class="btn btn-theme btn-outlined">Yes</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row">
|
||||
<br>
|
||||
<div style="text-align:center">
|
||||
|
@ -47,31 +73,29 @@
|
|||
<table id="teamsboard">
|
||||
<thead>
|
||||
<tr>
|
||||
<td><b>Status</b>
|
||||
<td class="text-center"><strong>Status</strong>
|
||||
</td>
|
||||
<td><b>Name</b>
|
||||
<td class="text-center"><strong>Name</strong>
|
||||
</td>
|
||||
<td><b>IP Address</b>
|
||||
<td class="text-center"><strong>Ports</strong>
|
||||
</td>
|
||||
<td><b>Memory</b>
|
||||
</td>
|
||||
<td><b>Disk</b>
|
||||
</td>
|
||||
<td><b>Settings</b>
|
||||
<td class="text-center"><strong>Settings</strong>
|
||||
</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for c in containers %}
|
||||
<tr>
|
||||
<td>{{ c.name }}</td>
|
||||
<td>{{ c.status }}</td>
|
||||
<td>{{ c.ip_address }}</td>
|
||||
<td>{{ c.memory }} <sub>MB</sub></td>
|
||||
<td>{{ c.disk }} <sub>GB</sub></td>
|
||||
<td>
|
||||
<td class="text-center">{{ c.status }}</td>
|
||||
<td class="text-center container_item" id="{{ c.id }}">{{ c.name }}</td>
|
||||
<td class="text-center">{{ c.ports }}</td>
|
||||
<td class="text-center">
|
||||
<span>
|
||||
<i class="fa fa-pencil-square-o"></i>
|
||||
{% if c.status != 'running' %}
|
||||
<i class="fa fa-play"></i>
|
||||
{% else %}
|
||||
<i class="fa fa-stop"></i>
|
||||
{% endif %}
|
||||
<i class="fa fa-times"></i>
|
||||
</span>
|
||||
</td>
|
||||
|
@ -84,4 +108,50 @@
|
|||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
|
||||
function load_confirm_modal(title, url, container_name){
|
||||
var modal = $('#confirm')
|
||||
modal.find('#confirm-container-name').text(container_name)
|
||||
modal.find('#confirm-container-title').text(title)
|
||||
modal.find('#confirm-container-method').text(title.toLowerCase())
|
||||
$('#confirm form').attr('action', url);
|
||||
$('#confirm').modal('show');
|
||||
}
|
||||
|
||||
$('#confirm-container').click(function(e){
|
||||
e.preventDefault();
|
||||
var id = $('#confirm input[name="id"]').val()
|
||||
var user_data = $('#confirm form').serializeArray()
|
||||
$.post($('#confirm form').attr('action'), $('#confirm form').serialize(), function(data){
|
||||
var data = $.parseJSON(JSON.stringify(data))
|
||||
if (data == "1"){
|
||||
location.reload()
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
$('.fa-times').click(function(){
|
||||
var elem = $(this).parent().parent().parent().find('.container_item');
|
||||
var container = elem.attr('id');
|
||||
var container_name = elem.text().trim();
|
||||
load_confirm_modal('Delete', '/admin/containers/'+container+'/delete', container_name)
|
||||
});
|
||||
|
||||
$('.fa-play').click(function(){
|
||||
var elem = $(this).parent().parent().parent().find('.container_item');
|
||||
var container = elem.attr('id');
|
||||
var container_name = elem.text().trim();
|
||||
load_confirm_modal('Start', '/admin/containers/'+container+'/start', container_name)
|
||||
});
|
||||
|
||||
$('.fa-stop').click(function(){
|
||||
var elem = $(this).parent().parent().parent().find('.container_item');
|
||||
var container = elem.attr('id');
|
||||
var container_name = elem.text().trim();
|
||||
load_confirm_modal('Stop', '/admin/containers/'+container+'/stop', container_name)
|
||||
});
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
|
@ -231,7 +231,7 @@ $('#send-user-email').click(function(e){
|
|||
});
|
||||
});
|
||||
|
||||
$('#delete-user').click(function(e){
|
||||
$('#delete-container').click(function(e){
|
||||
e.preventDefault();
|
||||
var id = $('#confirm input[name="id"]').val()
|
||||
var user_data = $('#confirm form').serializeArray()
|
||||
|
|
107
CTFd/utils.py
107
CTFd/utils.py
|
@ -5,7 +5,7 @@ from werkzeug.utils import secure_filename
|
|||
from functools import wraps
|
||||
from flask import current_app as app, g, request, redirect, url_for, session, render_template, abort
|
||||
from itsdangerous import Signer, BadSignature
|
||||
from socket import inet_aton, inet_ntoa
|
||||
from socket import inet_aton, inet_ntoa, socket
|
||||
from struct import unpack, pack
|
||||
from sqlalchemy.engine.url import make_url
|
||||
from sqlalchemy import create_engine
|
||||
|
@ -24,6 +24,7 @@ import smtplib
|
|||
import email
|
||||
import tempfile
|
||||
import subprocess
|
||||
import json
|
||||
|
||||
def init_logs(app):
|
||||
logger_keys = logging.getLogger('keys')
|
||||
|
@ -92,6 +93,7 @@ def init_utils(app):
|
|||
app.jinja_env.globals.update(can_register=can_register)
|
||||
app.jinja_env.globals.update(mailserver=mailserver)
|
||||
app.jinja_env.globals.update(ctf_name=ctf_name)
|
||||
app.jinja_env.globals.update(can_create_container=can_create_container)
|
||||
|
||||
@app.context_processor
|
||||
def inject_user():
|
||||
|
@ -206,11 +208,11 @@ def ctftime():
|
|||
# Within the two time bounds
|
||||
return True
|
||||
|
||||
if start < time.time() and end == 0:
|
||||
if start < time.time() and end == 0:
|
||||
# CTF starts on a date but never ends
|
||||
return True
|
||||
|
||||
if start == 0 and time.time() < end:
|
||||
if start == 0 and time.time() < end:
|
||||
# CTF started but ends at a date
|
||||
return True
|
||||
|
||||
|
@ -393,7 +395,16 @@ def can_create_container():
|
|||
return False
|
||||
|
||||
|
||||
def create_container(name, buildfile, files):
|
||||
def is_port_free(port):
|
||||
s = socket()
|
||||
result = s.connect_ex(('127.0.0.1', port))
|
||||
if result == 0:
|
||||
s.close()
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def create_image(name, buildfile, files):
|
||||
if not can_create_container():
|
||||
return False
|
||||
folder = tempfile.mkdtemp(prefix='ctfd')
|
||||
|
@ -404,14 +415,96 @@ def create_container(name, buildfile, files):
|
|||
for f in files:
|
||||
filename = os.path.basename(f.filename)
|
||||
f.save(os.path.join(folder, filename))
|
||||
|
||||
# repository name component must match "[a-z0-9](?:-*[a-z0-9])*(?:[._][a-z0-9](?:-*[a-z0-9])*)*"
|
||||
# docker build -f tmpfile.name -t name
|
||||
try:
|
||||
subprocess.call(['docker', 'build', '-f', tmpfile.name, '-t', name])
|
||||
cmd = ['docker', 'build', '-f', tmpfile.name, '-t', name, folder]
|
||||
print cmd
|
||||
subprocess.call(cmd)
|
||||
container = Containers(name, buildfile)
|
||||
db.session.add(container)
|
||||
db.session.commit()
|
||||
db.session.close()
|
||||
return True
|
||||
except subprocess.CalledProcessError:
|
||||
return False
|
||||
return False
|
||||
|
||||
|
||||
def delete_image(name):
|
||||
try:
|
||||
subprocess.call(['docker', 'rm', name])
|
||||
subprocess.call(['docker', 'rmi', name])
|
||||
return True
|
||||
except subprocess.CalledProcessError:
|
||||
return False
|
||||
|
||||
|
||||
def run_image(name):
|
||||
try:
|
||||
info = json.loads(subprocess.check_output(['docker', 'inspect', '--type=image', name]))
|
||||
|
||||
ports_asked = info[0]['Config']['ExposedPorts'].keys()
|
||||
ports_asked = [int(re.sub('[A-Za-z/]+', '', port)) for port in ports_asked]
|
||||
|
||||
cmd = ['docker', 'run', '-d']
|
||||
for port in ports_asked:
|
||||
if is_port_free(port):
|
||||
cmd.append('-p')
|
||||
cmd.append('{}:{}'.format(port, port))
|
||||
else:
|
||||
cmd.append('-p')
|
||||
ports_used.append('{}'.format(port))
|
||||
cmd += ['--name', name, name]
|
||||
print cmd
|
||||
subprocess.call(cmd)
|
||||
return True
|
||||
except subprocess.CalledProcessError:
|
||||
return False
|
||||
|
||||
|
||||
def container_start(name):
|
||||
try:
|
||||
cmd = ['docker', 'start', name]
|
||||
subprocess.call(cmd)
|
||||
return True
|
||||
except subprocess.CalledProcessError:
|
||||
return False
|
||||
|
||||
|
||||
def container_stop(name):
|
||||
try:
|
||||
cmd = ['docker', 'stop', name]
|
||||
subprocess.call(cmd)
|
||||
return True
|
||||
except subprocess.CalledProcessError:
|
||||
return False
|
||||
|
||||
|
||||
def container_status(name):
|
||||
try:
|
||||
data = json.loads(subprocess.check_output(['docker', 'inspect', '--type=container', name]))
|
||||
status = data[0]["State"]["Status"]
|
||||
return status
|
||||
except subprocess.CalledProcessError:
|
||||
return 'missing'
|
||||
|
||||
|
||||
def container_ports(name, verbose=False):
|
||||
try:
|
||||
info = json.loads(subprocess.check_output(['docker', 'inspect', '--type=container', name]))
|
||||
if verbose:
|
||||
ports = info[0]["NetworkSettings"]["Ports"]
|
||||
if not ports:
|
||||
return []
|
||||
final = []
|
||||
for port in ports.keys():
|
||||
final.append("".join([ports[port][0]["HostPort"], '->', port]))
|
||||
return final
|
||||
else:
|
||||
ports = info[0]['Config']['ExposedPorts'].keys()
|
||||
if not ports:
|
||||
return []
|
||||
ports = [int(re.sub('[A-Za-z/]+', '', port)) for port in ports_asked]
|
||||
return ports
|
||||
except subprocess.CalledProcessError:
|
||||
return []
|
|
@ -135,9 +135,7 @@ def teams(page):
|
|||
def team(teamid):
|
||||
if get_config('view_scoreboard_if_authed') and not authed():
|
||||
return redirect(url_for('auth.login', next=request.path))
|
||||
user = Teams.query.filter_by(id=teamid).first()
|
||||
if not user:
|
||||
abort(404)
|
||||
user = Teams.query.filter_by(id=teamid).first_or_404()
|
||||
solves = Solves.query.filter_by(teamid=teamid)
|
||||
awards = Awards.query.filter_by(teamid=teamid).all()
|
||||
score = user.score()
|
||||
|
|
Loading…
Reference in New Issue