From 69be11a56779bcd302b2a0a5cc16ccb54057a4c1 Mon Sep 17 00:00:00 2001 From: Kevin Chung Date: Sat, 12 Aug 2017 04:05:33 -0400 Subject: [PATCH] Extracted CTFd code --- README.md | 2 +- __init__.py | 82 ++++++++++++++++ config.html | 10 ++ models.py | 14 +++ requirements.txt | 0 templates/containers.html | 190 ++++++++++++++++++++++++++++++++++++++ utils.py | 151 ++++++++++++++++++++++++++++++ 7 files changed, 448 insertions(+), 1 deletion(-) create mode 100644 __init__.py create mode 100644 config.html create mode 100644 models.py create mode 100644 requirements.txt create mode 100644 templates/containers.html create mode 100644 utils.py diff --git a/README.md b/README.md index 4469aea..d0bed8d 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,2 @@ -# CTFd-Containers +# CTFd-Docker Plugin to give CTFd the ability to manage Docker containers diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..9672f28 --- /dev/null +++ b/__init__.py @@ -0,0 +1,82 @@ +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.models import db +from .models import Containers + +from . import utils + +def load(app): + app.db.create_all() + admin_containers = Blueprint('admin_containers', __name__, template_folder='templates') + + + @admin_containers.route('/admin/containers', methods=['GET']) + @admins_only + def list_container(): + containers = Containers.query.all() + for c in containers: + c.status = utils.container_status(c.name) + c.ports = ', '.join(utils.container_ports(c.name, verbose=True)) + return render_template('containers.html', containers=containers) + + + @admin_containers.route('/admin/containers//stop', methods=['POST']) + @admins_only + def stop_container(container_id): + container = Containers.query.filter_by(id=container_id).first_or_404() + if utils.container_stop(container.name): + return '1' + else: + return '0' + + + @admin_containers.route('/admin/containers//start', methods=['POST']) + @admins_only + def run_container(container_id): + container = Containers.query.filter_by(id=container_id).first_or_404() + if utils.container_status(container.name) == 'missing': + if utils.run_image(container.name): + return '1' + else: + return '0' + else: + if utils.container_start(container.name): + return '1' + else: + return '0' + + + @admin_containers.route('/admin/containers//delete', methods=['POST']) + @admins_only + def delete_container(container_id): + container = Containers.query.filter_by(id=container_id).first_or_404() + if utils.delete_image(container.name): + db.session.delete(container) + db.session.commit() + db.session.close() + return '1' + + + @admin_containers.route('/admin/containers/new', methods=['POST']) + @admins_only + def new_container(): + name = request.form.get('name') + if not set(name) <= set('abcdefghijklmnopqrstuvwxyz0123456789-_'): + return redirect(url_for('admin_containers.list_container')) + buildfile = request.form.get('buildfile') + files = request.files.getlist('files[]') + utils.create_image(name=name, buildfile=buildfile, files=files) + utils.run_image(name) + return redirect(url_for('admin_containers.list_container')) + + + @admin_containers.route('/admin/containers/import', methods=['POST']) + @admins_only + def import_container(): + name = request.form.get('name') + if not set(name) <= set('abcdefghijklmnopqrstuvwxyz0123456789-_'): + return redirect(url_for('admin_containers.list_container')) + utils.import_image(name=name) + return redirect(url_for('admin_containers.list_container')) + + app.register_blueprint(admin_containers) \ No newline at end of file diff --git a/config.html b/config.html new file mode 100644 index 0000000..ad8687f --- /dev/null +++ b/config.html @@ -0,0 +1,10 @@ +{% extends "admin/base.html" %} + +{% block content %} +{% endblock %} + +{% block scripts %} + +{% endblock %} \ No newline at end of file diff --git a/models.py b/models.py new file mode 100644 index 0000000..9fded52 --- /dev/null +++ b/models.py @@ -0,0 +1,14 @@ +from CTFd.models import db + + +class Containers(db.Model): + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(80)) + buildfile = db.Column(db.Text) + + def __init__(self, name, buildfile): + self.name = name + self.buildfile = buildfile + + def __repr__(self): + return "".format(self.id, self.name) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e69de29 diff --git a/templates/containers.html b/templates/containers.html new file mode 100644 index 0000000..b90c515 --- /dev/null +++ b/templates/containers.html @@ -0,0 +1,190 @@ +{% extends "admin/base.html" %} + +{% block content %} + + + + + + + + +
+
+
+

Containers

+ + +
+
+{% if containers %} + + + + + + + + + + + {% for c in containers %} + + + + + + + {% endfor %} + +
Status + Name + Ports + Settings +
{{ c.status }}{{ c.name }}{{ c.ports }} + + {% if c.status != 'running' %} + + {% else %} + + {% endif %} + + +
+{% endif %} +
+{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..075b2db --- /dev/null +++ b/utils.py @@ -0,0 +1,151 @@ +from CTFd.utils import admins_only, is_admin, cache +from CTFd.models import db +from .models import Containers + +import json +import subprocess +import socket +import tempfile + + +@cache.memoize() +def can_create_container(): + try: + subprocess.check_output(['docker', 'version']) + return True + except (subprocess.CalledProcessError, OSError): + return False + + +def import_image(name): + try: + info = json.loads(subprocess.check_output(['docker', 'inspect', '--type=image', name])) + container = Containers(name=name, buildfile=None) + db.session.add(container) + db.session.commit() + db.session.close() + return True + except subprocess.CalledProcessError: + return False + + +def create_image(name, buildfile, files): + if not can_create_container(): + return False + folder = tempfile.mkdtemp(prefix='ctfd') + tmpfile = tempfile.NamedTemporaryFile(dir=folder, delete=False) + tmpfile.write(buildfile) + tmpfile.close() + + for f in files: + if f.filename.strip(): + 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: + 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() + rmdir(folder) + return True + except subprocess.CalledProcessError: + return False + + +def is_port_free(port): + s = socket.socket() + result = s.connect_ex(('127.0.0.1', port)) + if result == 0: + s.close() + return False + return True + + +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])) + + try: + ports_asked = info[0]['Config']['ExposedPorts'].keys() + ports_asked = [int(re.sub('[A-Za-z/]+', '', port)) for port in ports_asked] + except KeyError: + ports_asked = [] + + cmd = ['docker', 'run', '-d'] + ports_used = [] + 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] + return ports + except subprocess.CalledProcessError: + return [] \ No newline at end of file