Initial Commit
parent
a52b749528
commit
7d779af2bc
|
@ -0,0 +1,64 @@
|
|||
# CTFd Docker Plugin
|
||||
This plugin for CTFd will allow your competing teams/users to start dockerized images for presented challenges. It adds a challenge type "docker" that can be assigned a specific docker image/tag. A few notable requirements:
|
||||
|
||||
* Docker Config must be set first. You can access this via `/admin/docker_config`. Currently supported config is pure http (no encryption/authentication) or full TLS with client certificate validation. Configuration information for TLS can be found here: https://docs.docker.com/engine/security/https/
|
||||
* This plugin is written so that challenges are stored by tags. For example, StormCTF stores all docker challenges for InfoSeCon2019 in the `stormctf/infosecon2019` repository. A challenge example would be `stormctf/infosecon2019:arbit`. This is how you would call the challenge when creating a new challenge.
|
||||
|
||||
|
||||
## Important Notes
|
||||
|
||||
* It is unknown if using the same tag twice will cause issues. This plugin was written to avoid this issue, but it has not been fully tested.
|
||||
* As with all plugins, please security test your Scoreboard before launching the CTF. This plugin has been tested and vetted in the StormCTF environment, but yours may vary.
|
||||
* In 2.3.3 a CTFd Configuration change is *REQUIRED*. Specifically, https://github.com/CTFd/CTFd/issues/1370. You will need to replace the function `get_configurable_plugins` with the one in the solution. This allows `config.json` to be a list, which allows multiple Menu items per plugin for the Plugins dropdown. You may want to change any other plugins you install to accommodate this. It's as simple as enclosing the curly braces with square braces. Example below.
|
||||
|
||||
```
|
||||
# Original config.json
|
||||
{
|
||||
"name": "Another Plugin",
|
||||
"route": "/admin/plugin/route"
|
||||
}
|
||||
```
|
||||
```
|
||||
# Modified config.json
|
||||
[{
|
||||
"name": "Another Plugin",
|
||||
"route": "/admin/plugin/route"
|
||||
}]
|
||||
```
|
||||
*NOTE: The above config.json modification only applies to OTHER plugins installed.*
|
||||
|
||||
*Requires flask_wtf*
|
||||
`pip install flask_wtf`
|
||||
|
||||
## Features
|
||||
|
||||
* Allows players to create their own docker container for docker challenges.
|
||||
* 5 minute rever timer.
|
||||
* 2 hour stale container nuke.
|
||||
* Status panel for Admins to manage docker containers currently active.
|
||||
* Support for client side validation TLS docker api connections (HIGHLY RECOMMENDED).
|
||||
* Docker container kill on solve.
|
||||
* (Mostly) Seamless integration with CTFd.
|
||||
* *Untested*: _Should_ be able to seamlessly integrate with other challenge types.
|
||||
|
||||
## Installation / Configuration
|
||||
|
||||
* Make the above required code change in CTFd 2.3.3 (`get_configurable_plugins`).
|
||||
* Drop the folder `docker_challenges` into `CTFd/CTFd/plugins` (Exactly this name).
|
||||
* Restart CTFd.
|
||||
* Navigate to `/admin/docker_config`. Add your configuration information. Click Submit.
|
||||
* Add your required repositories for this CTF. You can select multiple by holding CTRL when clicking. Click Submit.
|
||||
* Click Challenges, Select `docker` for challenge type. Create a challenge as normal, but select the correct docker tag for this challenge.
|
||||
* Double check the front end shows "Start Docker Instance" on the challenge.
|
||||
* Confirm users are able to start/revert and access docker challenges.
|
||||
* Host an awesome CTF!
|
||||
|
||||
### Update: 20200504
|
||||
Works with 2.3.3
|
||||
|
||||
* Updated the entire plugin to work with the new CTFd.
|
||||
|
||||
#### Credits
|
||||
|
||||
* https://github.com/offsecginger (Twitter: @offsec_ginger)
|
||||
* Jaime Geiger (For Original Plugin assistance) (Twitter: @jgeigerm)
|
|
@ -0,0 +1,655 @@
|
|||
from CTFd.plugins.challenges import BaseChallenge, CHALLENGE_CLASSES, get_chal_class
|
||||
from CTFd.plugins.flags import get_flag_class
|
||||
from CTFd.utils.user import get_ip
|
||||
from CTFd.utils.uploads import delete_file
|
||||
from CTFd.plugins import register_plugin_assets_directory, bypass_csrf_protection
|
||||
from CTFd.schemas.tags import TagSchema
|
||||
from CTFd.models import db, ma, Challenges, Teams, Users, Solves, Fails, Flags, Files, Hints, Tags, ChallengeFiles
|
||||
from CTFd.utils.decorators import admins_only, authed_only, during_ctf_time_only, require_verified_emails
|
||||
from CTFd.utils.decorators.visibility import check_challenge_visibility, check_score_visibility
|
||||
from CTFd.utils.user import get_current_team
|
||||
from CTFd.utils.user import get_current_user
|
||||
from CTFd.utils.user import is_admin, authed
|
||||
from CTFd.utils.config import is_teams_mode
|
||||
from CTFd.api import CTFd_API_v1
|
||||
from CTFd.api.v1.scoreboard import ScoreboardDetail
|
||||
import CTFd.utils.scores
|
||||
from CTFd.api.v1.challenges import ChallengeList, Challenge
|
||||
from flask_restplus import Namespace, Resource
|
||||
from flask import request, Blueprint, jsonify, abort, render_template, url_for, redirect, session
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import TextField, SubmitField, BooleanField, HiddenField, FileField, SelectMultipleField
|
||||
from wtforms.validators import DataRequired, ValidationError
|
||||
from werkzeug.utils import secure_filename
|
||||
import requests
|
||||
import tempfile
|
||||
from CTFd.utils.dates import unix_time
|
||||
from datetime import datetime
|
||||
import json
|
||||
import hashlib
|
||||
import random
|
||||
from CTFd.plugins import register_admin_plugin_menu_bar
|
||||
|
||||
class DockerConfig(db.Model):
|
||||
"""
|
||||
Docker Config Model. This model stores the config for docker API connections.
|
||||
"""
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
hostname = db.Column("hostname",db.String(64), index=True)
|
||||
tls_enabled = db.Column("tls_enabled",db.Boolean, index=True)
|
||||
ca_cert = db.Column("ca_cert",db.String, index=True)
|
||||
client_cert = db.Column("client_cert",db.String, index=True)
|
||||
client_key = db.Column("client_key",db.String, index=True)
|
||||
repositories = db.Column("repositories",db.String, index=True)
|
||||
|
||||
class DockerChallengeTracker(db.Model):
|
||||
"""
|
||||
Docker Container Tracker. This model stores the users/teams active docker containers.
|
||||
"""
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
team_id = db.Column("team_id",db.String, index=True)
|
||||
user_id = db.Column("user_id",db.String, index=True)
|
||||
docker_image = db.Column("docker_image",db.String, index=True)
|
||||
timestamp = db.Column("timestamp",db.Integer, index=True)
|
||||
revert_time = db.Column("revert_time",db.Integer, index=True)
|
||||
instance_id = db.Column("instance_id",db.String, index=True)
|
||||
ports = db.Column('ports', db.String, index=True)
|
||||
host = db.Column('host', db.String, index=True)
|
||||
|
||||
class DockerConfigForm(FlaskForm):
|
||||
"""
|
||||
Docker Config Form. This Form Handles the Docker Config data.
|
||||
"""
|
||||
id = HiddenField()
|
||||
hostname = TextField('Docker Hostname', render_kw={"placeholder": "10.10.10.10:2376", "autofocus" : "true"}, validators=[DataRequired("Hostname name is required")])
|
||||
tls_enabled = BooleanField('TLS Enabled?')
|
||||
ca_cert = FileField('CA Cert')
|
||||
client_cert = FileField('Client Cert')
|
||||
client_key = FileField('Client Key')
|
||||
repositories = SelectMultipleField('Repositories')
|
||||
submit = SubmitField('Submit')
|
||||
|
||||
def define_docker_admin(app):
|
||||
admin_docker_config = Blueprint('admin_docker_config', __name__, template_folder='templates', static_folder='assets')
|
||||
@admin_docker_config.route("/admin/docker_config", methods=["GET", "POST"])
|
||||
@admins_only
|
||||
def docker_config():
|
||||
docker = DockerConfig.query.filter_by(id=1).first()
|
||||
form = DockerConfigForm()
|
||||
if request.method == "POST":
|
||||
if docker:
|
||||
b = docker
|
||||
else:
|
||||
b = DockerConfig()
|
||||
try: ca_cert = request.files['ca_cert'].stream.read()
|
||||
except: ca_cert = ''
|
||||
try: client_cert = request.files['client_cert'].stream.read()
|
||||
except: client_cert = ''
|
||||
try: client_key = request.files['client_key'].stream.read()
|
||||
except: client_key = ''
|
||||
if len(ca_cert) != 0: b.ca_cert = ca_cert
|
||||
if len(client_cert) != 0: b.client_cert = client_cert
|
||||
if len(client_key) != 0: b.client_key = client_key
|
||||
b.hostname = form.hostname.data
|
||||
b.tls_enabled = form.tls_enabled.data
|
||||
if not b.tls_enabled:
|
||||
b.ca_cert = None
|
||||
b.client_cert = None
|
||||
b.client_key = None
|
||||
b.repositories = ','.join(form.repositories.data) or None
|
||||
db.session.add(b)
|
||||
db.session.commit()
|
||||
docker = DockerConfig.query.filter_by(id=1).first()
|
||||
try:
|
||||
repos = get_repositories(docker)
|
||||
except:
|
||||
repos = list()
|
||||
if len(repos) == 0:
|
||||
form.repositories.choices = [("ERROR","Failed to Connect to Docker")]
|
||||
else:
|
||||
form.repositories.choices = [(d, d) for d in repos]
|
||||
dconfig = DockerConfig.query.first()
|
||||
try:
|
||||
selected_repos = dconfig.repositories.split(',')
|
||||
except:
|
||||
selected_repos = []
|
||||
return render_template("docker_config.html", config=dconfig, form=form, repos=selected_repos)
|
||||
app.register_blueprint(admin_docker_config)
|
||||
|
||||
|
||||
def define_docker_status(app):
|
||||
admin_docker_status = Blueprint('admin_docker_status', __name__, template_folder='templates', static_folder='assets')
|
||||
@admin_docker_status.route("/admin/docker_status", methods=["GET", "POST"])
|
||||
@admins_only
|
||||
def docker_admin():
|
||||
docker_config = DockerConfig.query.filter_by(id=1).first()
|
||||
docker_tracker = DockerChallengeTracker.query.all()
|
||||
for i in docker_tracker:
|
||||
if is_teams_mode():
|
||||
name = Teams.query.filter_by(id=i.team_id).first()
|
||||
i.team_id = name.name
|
||||
else:
|
||||
name = Users.query.filter_by(id=i.user_id).first()
|
||||
i.user_id = name.name
|
||||
return render_template("admin_docker_status.html", dockers=docker_tracker)
|
||||
app.register_blueprint(admin_docker_status)
|
||||
|
||||
|
||||
kill_container = Namespace("nuke", description='Endpoint to nuke containers')
|
||||
@kill_container.route("", methods=['POST','GET'])
|
||||
class KillContainerAPI(Resource):
|
||||
@admins_only
|
||||
def get(self):
|
||||
container = request.args.get('container')
|
||||
full = request.args.get('all')
|
||||
docker_config = DockerConfig.query.filter_by(id=1).first()
|
||||
docker_tracker = DockerChallengeTracker.query.all()
|
||||
if full == "true":
|
||||
for c in docker_tracker:
|
||||
delete_container(docker_config, c.instance_id)
|
||||
DockerChallengeTracker.query.filter_by(instance_id=c.instance_id).delete()
|
||||
db.session.commit()
|
||||
db.session.close()
|
||||
elif container != 'null' and container in [c.instance_id for c in docker_tracker]:
|
||||
delete_container(docker_config, container)
|
||||
DockerChallengeTracker.query.filter_by(instance_id=container).delete()
|
||||
db.session.commit()
|
||||
db.session.close()
|
||||
else:
|
||||
return False
|
||||
return True
|
||||
|
||||
# For the Docker Config Page. Gets the Current Repositories available on the Docker Server.
|
||||
def get_repositories(docker, tags=False, repos=False):
|
||||
tls = docker.tls_enabled
|
||||
if not tls:
|
||||
prefix = 'http'
|
||||
else:
|
||||
prefix = 'https'
|
||||
try:
|
||||
ca = docker.ca_cert
|
||||
client = docker.client_cert
|
||||
ckey = docker.client_key
|
||||
ca_file = tempfile.NamedTemporaryFile(delete=False)
|
||||
ca_file.write(ca)
|
||||
ca_file.seek(0)
|
||||
client_file = tempfile.NamedTemporaryFile(delete=False)
|
||||
client_file.write(client)
|
||||
client_file.seek(0)
|
||||
key_file = tempfile.NamedTemporaryFile(delete=False)
|
||||
key_file.write(ckey)
|
||||
key_file.seek(0)
|
||||
CERT = (client_file.name,key_file.name)
|
||||
except:
|
||||
return []
|
||||
host = docker.hostname
|
||||
URL_TEMPLATE = '%s://%s' % (prefix, host)
|
||||
if tls:
|
||||
try:
|
||||
r = requests.get(url="%s/images/json?all=1" % URL_TEMPLATE, cert=CERT, verify=ca_file.name)
|
||||
except:
|
||||
return []
|
||||
else:
|
||||
try:
|
||||
r = requests.get(url="%s/images/json?all=1" % URL_TEMPLATE)
|
||||
except:
|
||||
return []
|
||||
result = list()
|
||||
for i in r.json():
|
||||
if not i['RepoTags'] == None:
|
||||
if not i['RepoTags'][0].split(':')[0] == '<none>':
|
||||
if repos:
|
||||
if not i['RepoTags'][0].split(':')[0] in repos:
|
||||
continue
|
||||
if not tags:
|
||||
result.append(i['RepoTags'][0].split(':')[0])
|
||||
else:
|
||||
result.append(i['RepoTags'][0])
|
||||
return list(set(result))
|
||||
|
||||
def get_unavailable_ports(docker):
|
||||
tls = docker.tls_enabled
|
||||
if not tls:
|
||||
prefix = 'http'
|
||||
else:
|
||||
prefix = 'https'
|
||||
try:
|
||||
ca = docker.ca_cert
|
||||
client = docker.client_cert
|
||||
ckey = docker.client_key
|
||||
ca_file = tempfile.NamedTemporaryFile(delete=False)
|
||||
ca_file.write(ca)
|
||||
ca_file.seek(0)
|
||||
client_file = tempfile.NamedTemporaryFile(delete=False)
|
||||
client_file.write(client)
|
||||
client_file.seek(0)
|
||||
key_file = tempfile.NamedTemporaryFile(delete=False)
|
||||
key_file.write(ckey)
|
||||
key_file.seek(0)
|
||||
CERT = (client_file.name,key_file.name)
|
||||
except:
|
||||
return []
|
||||
host = docker.hostname
|
||||
URL_TEMPLATE = '%s://%s' % (prefix, host)
|
||||
r = requests.get(url="%s/containers/json?all=1" % URL_TEMPLATE, cert=CERT, verify=ca_file.name)
|
||||
result = list()
|
||||
for i in r.json():
|
||||
if not i['Ports'] == []:
|
||||
for p in i['Ports']:
|
||||
result.append(p['PublicPort'])
|
||||
return result
|
||||
|
||||
def get_required_ports(docker, image):
|
||||
tls = docker.tls_enabled
|
||||
if not tls:
|
||||
prefix = 'http'
|
||||
else:
|
||||
prefix = 'https'
|
||||
try:
|
||||
ca = docker.ca_cert
|
||||
client = docker.client_cert
|
||||
ckey = docker.client_key
|
||||
ca_file = tempfile.NamedTemporaryFile(delete=False)
|
||||
ca_file.write(ca)
|
||||
ca_file.seek(0)
|
||||
client_file = tempfile.NamedTemporaryFile(delete=False)
|
||||
client_file.write(client)
|
||||
client_file.seek(0)
|
||||
key_file = tempfile.NamedTemporaryFile(delete=False)
|
||||
key_file.write(ckey)
|
||||
key_file.seek(0)
|
||||
CERT = (client_file.name,key_file.name)
|
||||
except:
|
||||
return []
|
||||
host = docker.hostname
|
||||
URL_TEMPLATE = '%s://%s' % (prefix, host)
|
||||
r = requests.get(url="%s/images/%s/json?all=1" % (URL_TEMPLATE, image), cert=CERT, verify=ca_file.name)
|
||||
result = r.json()['ContainerConfig']['ExposedPorts'].keys()
|
||||
return result
|
||||
|
||||
|
||||
def create_container(docker, image, team, portbl):
|
||||
tls = docker.tls_enabled
|
||||
if not tls:
|
||||
prefix = 'http'
|
||||
else:
|
||||
prefix = 'https'
|
||||
try:
|
||||
ca = docker.ca_cert
|
||||
client = docker.client_cert
|
||||
ckey = docker.client_key
|
||||
ca_file = tempfile.NamedTemporaryFile(delete=False)
|
||||
ca_file.write(ca)
|
||||
ca_file.seek(0)
|
||||
client_file = tempfile.NamedTemporaryFile(delete=False)
|
||||
client_file.write(client)
|
||||
client_file.seek(0)
|
||||
key_file = tempfile.NamedTemporaryFile(delete=False)
|
||||
key_file.write(ckey)
|
||||
key_file.seek(0)
|
||||
CERT = (client_file.name,key_file.name)
|
||||
except:
|
||||
return []
|
||||
host = docker.hostname
|
||||
URL_TEMPLATE = '%s://%s' % (prefix, host)
|
||||
needed_ports = get_required_ports(docker, image)
|
||||
team = hashlib.md5(team.encode("utf-8")).hexdigest()[:10]
|
||||
container_name = "%s_%s" % (image.split(':')[1], team)
|
||||
assigned_ports = dict()
|
||||
for i in needed_ports:
|
||||
while True:
|
||||
assigned_port = random.choice(range(30000,60000))
|
||||
if assigned_port not in portbl:
|
||||
assigned_ports['%s/tcp' % assigned_port] = { }
|
||||
break
|
||||
ports = dict()
|
||||
bindings = dict()
|
||||
tmp_ports = assigned_ports.keys()
|
||||
for i in needed_ports:
|
||||
ports[i] = { }
|
||||
bindings[i] = [{ "HostPort": tmp_ports.pop()}]
|
||||
headers = {'Content-Type': "application/json"}
|
||||
data = json.dumps({"Image": image, "ExposedPorts": ports, "HostConfig" : { "PortBindings" : bindings } })
|
||||
r = requests.post(url="%s/containers/create?name=%s" % (URL_TEMPLATE, container_name), cert=CERT, verify=ca_file.name, data=data, headers=headers)
|
||||
result = r.json()
|
||||
s = requests.post(url="%s/containers/%s/start" % (URL_TEMPLATE, result['Id']), cert=CERT, verify=ca_file.name, headers=headers)
|
||||
return result,data
|
||||
|
||||
def delete_container(docker, instance_id):
|
||||
tls = docker.tls_enabled
|
||||
if not tls:
|
||||
prefix = 'http'
|
||||
else:
|
||||
prefix = 'https'
|
||||
try:
|
||||
ca = docker.ca_cert
|
||||
client = docker.client_cert
|
||||
ckey = docker.client_key
|
||||
ca_file = tempfile.NamedTemporaryFile(delete=False)
|
||||
ca_file.write(ca)
|
||||
ca_file.seek(0)
|
||||
client_file = tempfile.NamedTemporaryFile(delete=False)
|
||||
client_file.write(client)
|
||||
client_file.seek(0)
|
||||
key_file = tempfile.NamedTemporaryFile(delete=False)
|
||||
key_file.write(ckey)
|
||||
key_file.seek(0)
|
||||
CERT = (client_file.name,key_file.name)
|
||||
except:
|
||||
return []
|
||||
host = docker.hostname
|
||||
URL_TEMPLATE = '%s://%s' % (prefix, host)
|
||||
headers = {'Content-Type': "application/json"}
|
||||
r = requests.delete(url="%s/containers/%s?force=true" % (URL_TEMPLATE, instance_id), cert=CERT, verify=ca_file.name, headers=headers)
|
||||
return True
|
||||
|
||||
class DockerChallengeType(BaseChallenge):
|
||||
id = "docker"
|
||||
name = "docker"
|
||||
templates = {
|
||||
'create': '/plugins/docker_challenges/assets/create.html',
|
||||
'update': '/plugins/docker_challenges/assets/update.html',
|
||||
'view': '/plugins/docker_challenges/assets/view.html',
|
||||
}
|
||||
scripts = {
|
||||
'create': '/plugins/docker_challenges/assets/create.js',
|
||||
'update': '/plugins/docker_challenges/assets/update.js',
|
||||
'view': '/plugins/docker_challenges/assets/view.js',
|
||||
}
|
||||
route = '/plugins/docker_challenges/assets'
|
||||
blueprint = Blueprint('docker_challenges', __name__, template_folder='templates', static_folder='assets')
|
||||
|
||||
@staticmethod
|
||||
def update(challenge, request):
|
||||
"""
|
||||
This method is used to update the information associated with a challenge. This should be kept strictly to the
|
||||
Challenges table and any child tables.
|
||||
|
||||
:param challenge:
|
||||
:param request:
|
||||
:return:
|
||||
"""
|
||||
data = request.form or request.get_json()
|
||||
for attr, value in data.items():
|
||||
setattr(challenge, attr, value)
|
||||
|
||||
db.session.commit()
|
||||
return challenge
|
||||
|
||||
@staticmethod
|
||||
def delete(challenge):
|
||||
"""
|
||||
This method is used to delete the resources used by a challenge.
|
||||
NOTE: Will need to kill all containers here
|
||||
|
||||
:param challenge:
|
||||
:return:
|
||||
"""
|
||||
Fails.query.filter_by(challenge_id=challenge.id).delete()
|
||||
Solves.query.filter_by(challenge_id=challenge.id).delete()
|
||||
Flags.query.filter_by(challenge_id=challenge.id).delete()
|
||||
files = ChallengeFiles.query.filter_by(challenge_id=challenge.id).all()
|
||||
for f in files:
|
||||
delete_file(f.id)
|
||||
ChallengeFiles.query.filter_by(challenge_id=challenge.id).delete()
|
||||
Tags.query.filter_by(challenge_id=challenge.id).delete()
|
||||
Hints.query.filter_by(challenge_id=challenge.id).delete()
|
||||
Challenges.query.filter_by(id=challenge.id).delete()
|
||||
DockerChallenge.query.filter_by(id=challenge.id).delete()
|
||||
db.session.commit()
|
||||
|
||||
@staticmethod
|
||||
def read(challenge):
|
||||
"""
|
||||
This method is in used to access the data of a challenge in a format processable by the front end.
|
||||
|
||||
:param challenge:
|
||||
:return: Challenge object, data dictionary to be returned to the user
|
||||
"""
|
||||
challenge = DockerChallenge.query.filter_by(id=challenge.id).first()
|
||||
data = {
|
||||
'id': challenge.id,
|
||||
'name': challenge.name,
|
||||
'value': challenge.value,
|
||||
'docker_image': challenge.docker_image,
|
||||
'description': challenge.description,
|
||||
'category': challenge.category,
|
||||
'state': challenge.state,
|
||||
'max_attempts': challenge.max_attempts,
|
||||
'type': challenge.type,
|
||||
'type_data': {
|
||||
'id': DockerChallengeType.id,
|
||||
'name': DockerChallengeType.name,
|
||||
'templates': DockerChallengeType.templates,
|
||||
'scripts': DockerChallengeType.scripts,
|
||||
}
|
||||
}
|
||||
return data
|
||||
|
||||
@staticmethod
|
||||
def create(request):
|
||||
"""
|
||||
This method is used to process the challenge creation request.
|
||||
|
||||
:param request:
|
||||
:return:
|
||||
"""
|
||||
data = request.form or request.get_json()
|
||||
challenge = DockerChallenge(**data)
|
||||
db.session.add(challenge)
|
||||
db.session.commit()
|
||||
return challenge
|
||||
|
||||
@staticmethod
|
||||
def attempt(challenge, request):
|
||||
"""
|
||||
This method is used to check whether a given input is right or wrong. It does not make any changes and should
|
||||
return a boolean for correctness and a string to be shown to the user. It is also in charge of parsing the
|
||||
user's input from the request itself.
|
||||
|
||||
:param challenge: The Challenge object from the database
|
||||
:param request: The request the user submitted
|
||||
:return: (boolean, string)
|
||||
"""
|
||||
data = request.form or request.get_json()
|
||||
submission = data["submission"].strip()
|
||||
flags = Flags.query.filter_by(challenge_id=challenge.id).all()
|
||||
for flag in flags:
|
||||
if get_flag_class(flag.type).compare(flag, submission):
|
||||
return True, "Correct"
|
||||
return False, "Incorrect"
|
||||
|
||||
@staticmethod
|
||||
def solve(user, team, challenge, request):
|
||||
"""
|
||||
This method is used to insert Solves into the database in order to mark a challenge as solved.
|
||||
|
||||
:param team: The Team object from the database
|
||||
:param chal: The Challenge object from the database
|
||||
:param request: The request the user submitted
|
||||
:return:
|
||||
"""
|
||||
data = request.form or request.get_json()
|
||||
submission = data["submission"].strip()
|
||||
docker = DockerConfig.query.filter_by(id=1).first()
|
||||
try:
|
||||
if is_teams_mode():
|
||||
docker_containers = DockerChallengeTracker.query.filter_by(docker_image=challenge.docker_image).filter_by(team_id=team.id).first()
|
||||
else:
|
||||
docker_containers = DockerChallengeTracker.query.filter_by(docker_image=challenge.docker_image).filter_by(user_id=user.id).first()
|
||||
delete_container(docker, docker_containers.instance_id)
|
||||
DockerChallengeTracker.query.filter_by(instance_id=docker_containers.instance_id).delete()
|
||||
except:
|
||||
pass
|
||||
solve = Solves(
|
||||
user_id=user.id,
|
||||
team_id=team.id if team else None,
|
||||
challenge_id=challenge.id,
|
||||
ip=get_ip(req=request),
|
||||
provided=submission,
|
||||
)
|
||||
db.session.add(solve)
|
||||
db.session.commit()
|
||||
db.session.close()
|
||||
|
||||
@staticmethod
|
||||
def fail(user, team, challenge, request):
|
||||
"""
|
||||
This method is used to insert Fails into the database in order to mark an answer incorrect.
|
||||
|
||||
:param team: The Team object from the database
|
||||
:param chal: The Challenge object from the database
|
||||
:param request: The request the user submitted
|
||||
:return:
|
||||
"""
|
||||
data = request.form or request.get_json()
|
||||
submission = data["submission"].strip()
|
||||
wrong = Fails(
|
||||
user_id=user.id,
|
||||
team_id=team.id if team else None,
|
||||
challenge_id=challenge.id,
|
||||
ip=get_ip(request),
|
||||
provided=submission,
|
||||
)
|
||||
db.session.add(wrong)
|
||||
db.session.commit()
|
||||
db.session.close()
|
||||
|
||||
class DockerChallenge(Challenges):
|
||||
__mapper_args__ = {'polymorphic_identity': 'docker'}
|
||||
id = db.Column(None, db.ForeignKey('challenges.id'), primary_key=True)
|
||||
docker_image = db.Column(db.String, index=True)
|
||||
|
||||
# API
|
||||
container_namespace = Namespace("container", description='Endpoint to interact with containers')
|
||||
@container_namespace.route("", methods=['POST','GET'])
|
||||
class ContainerAPI(Resource):
|
||||
@authed_only
|
||||
# I wish this was Post... Issues with API/CSRF and whatnot. Open to a Issue solving this.
|
||||
def get(self):
|
||||
container = request.args.get('name')
|
||||
if not container:
|
||||
return abort(403)
|
||||
docker = DockerConfig.query.filter_by(id=1).first()
|
||||
containers = DockerChallengeTracker.query.all()
|
||||
if container not in get_repositories(docker, tags=True):
|
||||
return abort(403)
|
||||
if is_teams_mode():
|
||||
session = get_current_team()
|
||||
# First we'll delete all old docker containers (+2 hours)
|
||||
for i in containers:
|
||||
if int(session.id) == int(i.team_id) and (unix_time(datetime.utcnow()) - int(i.timestamp)) >= 7200:
|
||||
delete_container(docker, i.instance_id)
|
||||
DockerChallengeTracker.query.filter_by(instance_id=i.instance_id).delete()
|
||||
db.session.commit()
|
||||
check = DockerChallengeTracker.query.filter_by(team_id=session.id).filter_by(docker_image=container).first()
|
||||
else:
|
||||
session = get_current_user()
|
||||
for i in containers:
|
||||
if int(session.id) == int(i.user_id) and (unix_time(datetime.utcnow()) - int(i.timestamp)) >= 7200:
|
||||
delete_container(docker, i.instance_id)
|
||||
DockerChallengeTracker.query.filter_by(instance_id=i.instance_id).delete()
|
||||
db.session.commit()
|
||||
check = DockerChallengeTracker.query.filter_by(user_id=session.id).filter_by(docker_image=container).first()
|
||||
# If this container is already created, we don't need another one.
|
||||
if check != None and not (unix_time(datetime.utcnow()) - int(check.timestamp)) >= 300:
|
||||
return abort(403)
|
||||
# The exception would be if we are reverting a box. So we'll delete it if it exists and has been around for more than 5 minutes.
|
||||
elif check != None:
|
||||
delete_container(docker, check.instance_id)
|
||||
if is_teams_mode():
|
||||
DockerChallengeTracker.query.filter_by(team_id=session.id).filter_by(docker_image=container).delete()
|
||||
else:
|
||||
DockerChallengeTracker.query.filter_by(user_id=session.id).filter_by(docker_image=container).delete()
|
||||
db.session.commit()
|
||||
portsbl = get_unavailable_ports(docker)
|
||||
create = create_container(docker,container,session.name,portsbl)
|
||||
ports = json.loads(create[1])['HostConfig']['PortBindings'].values()
|
||||
entry = DockerChallengeTracker(
|
||||
team_id = session.id if is_teams_mode() else None,
|
||||
user_id = session.id if not is_teams_mode() else None,
|
||||
docker_image = container,
|
||||
timestamp = unix_time(datetime.utcnow()),
|
||||
revert_time = unix_time(datetime.utcnow()) + 300,
|
||||
instance_id = create[0]['Id'],
|
||||
ports = ','.join([p[0]['HostPort'] for p in ports]),
|
||||
host = str(docker.hostname).split(':')[0]
|
||||
)
|
||||
db.session.add(entry)
|
||||
db.session.commit()
|
||||
db.session.close()
|
||||
return
|
||||
|
||||
active_docker_namespace = Namespace("docker", description='Endpoint to retrieve User Docker Image Status')
|
||||
@active_docker_namespace.route("", methods=['POST','GET'])
|
||||
class DockerStatus(Resource):
|
||||
"""
|
||||
The Purpose of this API is to retrieve a public JSON string of all docker containers
|
||||
in use by the current team/user.
|
||||
"""
|
||||
@authed_only
|
||||
def get(self):
|
||||
docker = DockerConfig.query.filter_by(id=1).first()
|
||||
if is_teams_mode():
|
||||
session = get_current_team()
|
||||
tracker = DockerChallengeTracker.query.filter_by(team_id=session.id)
|
||||
else:
|
||||
session = get_current_user()
|
||||
tracker = DockerChallengeTracker.query.filter_by(user_id=session.id)
|
||||
data = list()
|
||||
for i in tracker:
|
||||
data.append({
|
||||
'id' : i.id,
|
||||
'team_id' : i.team_id,
|
||||
'user_id' : i.user_id,
|
||||
'docker_image' : i.docker_image,
|
||||
'timestamp' : i.timestamp,
|
||||
'revert_time' : i.revert_time,
|
||||
'instance_id' : i.instance_id,
|
||||
'ports' : i.ports.split(','),
|
||||
'host' : str(docker.hostname).split(':')[0]
|
||||
})
|
||||
return {
|
||||
'success' : True,
|
||||
'data' : data
|
||||
}
|
||||
|
||||
docker_namespace = Namespace("docker", description='Endpoint to retrieve dockerstuff')
|
||||
@docker_namespace.route("", methods=['POST','GET'])
|
||||
class DockerAPI(Resource):
|
||||
"""
|
||||
This is for creating Docker Challenges. The purpose of this API is to populate the Docker Image Select form
|
||||
object in the Challenge Creation Screen.
|
||||
"""
|
||||
@admins_only
|
||||
def get(self):
|
||||
docker = DockerConfig.query.filter_by(id=1).first()
|
||||
images = get_repositories(docker, tags=True, repos=docker.repositories)
|
||||
if images:
|
||||
data = list()
|
||||
for i in images:
|
||||
data.append({'name':i})
|
||||
return {
|
||||
'success' : True,
|
||||
'data' : data
|
||||
}
|
||||
else:
|
||||
return {
|
||||
'success' : False,
|
||||
'data' : [
|
||||
{
|
||||
'name':'Error in Docker Config!'
|
||||
}
|
||||
]
|
||||
}, 400
|
||||
|
||||
def load(app):
|
||||
app.db.create_all()
|
||||
CHALLENGE_CLASSES['docker'] = DockerChallengeType
|
||||
register_plugin_assets_directory(app, base_path='/plugins/docker_challenges/assets')
|
||||
define_docker_admin(app)
|
||||
define_docker_status(app)
|
||||
CTFd_API_v1.add_namespace(docker_namespace, '/docker')
|
||||
CTFd_API_v1.add_namespace(container_namespace, '/container')
|
||||
CTFd_API_v1.add_namespace(active_docker_namespace, '/docker_status')
|
||||
CTFd_API_v1.add_namespace(kill_container, '/nuke')
|
|
@ -0,0 +1,70 @@
|
|||
<form method="POST" action="{{ script_root }}/admin/challenge/new" enctype="multipart/form-data" name='docker_form'>
|
||||
<div class="form-group">
|
||||
<label>
|
||||
Name:<br>
|
||||
<small class="form-text text-muted">
|
||||
The name of your challenge
|
||||
</small>
|
||||
</label>
|
||||
<input type="text" class="form-control" name="name" placeholder="Enter challenge name">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>
|
||||
Category:<br>
|
||||
<small class="form-text text-muted">
|
||||
The category of your challenge
|
||||
</small>
|
||||
</label>
|
||||
<input type="text" class="form-control" name="category" placeholder="Enter challenge category">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="DockerImage" id='dockerimage_label'>Docker Image:
|
||||
<i class="far fa-question-circle text-muted cursor-help" data-toggle="tooltip" data-placement="right" title="The docker image for your challenge"></i>
|
||||
</label>
|
||||
<select id="dockerimage_select" name="docker_image" class="form-control" required></select>
|
||||
</div>
|
||||
<ul class="nav nav-tabs" role="tablist" id="new-desc-edit">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" href="#new-desc-write" aria-controls="home" role="tab"
|
||||
data-toggle="tab">Write</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#new-desc-preview" aria-controls="home" role="tab" data-toggle="tab">Preview</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content">
|
||||
<div role="tabpanel" class="tab-pane active" id="new-desc-write">
|
||||
<div class="form-group">
|
||||
<label>
|
||||
Message:<br>
|
||||
<small class="form-text text-muted">
|
||||
Use this to give a brief introduction to your challenge.
|
||||
</small>
|
||||
</label>
|
||||
<textarea id="new-desc-editor" class="form-control" name="description" rows="10"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane content" id="new-desc-preview" style="height:234px; overflow-y: scroll;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>
|
||||
Value:<br>
|
||||
<small class="form-text text-muted">
|
||||
This is how many points are rewarded for solving this challenge.
|
||||
</small>
|
||||
</label>
|
||||
<input type="number" class="form-control" name="value" placeholder="Enter value" required>
|
||||
</div>
|
||||
|
||||
<input type="hidden" name="state" value="hidden">
|
||||
<input type="hidden" name="type" value="standard">
|
||||
<input type="hidden" value="docker" name="type" id="chaltype">
|
||||
|
||||
<div class="form-group">
|
||||
<button class="btn btn-primary float-right create-challenge-submit" type="submit">Create</button>
|
||||
</div>
|
||||
</form>
|
|
@ -0,0 +1,26 @@
|
|||
CTFd.plugin.run((_CTFd) => {
|
||||
const $ = _CTFd.lib.$
|
||||
const md = _CTFd.lib.markdown()
|
||||
$('a[href="#new-desc-preview"]').on('shown.bs.tab', function (event) {
|
||||
if (event.target.hash == '#new-desc-preview') {
|
||||
var editor_value = $('#new-desc-editor').val();
|
||||
$(event.target.hash).html(
|
||||
md.render(editor_value)
|
||||
);
|
||||
}
|
||||
});
|
||||
$(document).ready(function(){
|
||||
$('[data-toggle="tooltip"]').tooltip();
|
||||
$.getJSON("/api/v1/docker", function(result){
|
||||
$.each(result['data'], function(i, item){
|
||||
if (item.name == 'Error in Docker Config!') {
|
||||
document.docker_form.dockerimage_select.disabled = true;
|
||||
$("label[for='DockerImage']").text('Docker Image ' + item.name)
|
||||
}
|
||||
else {
|
||||
$("#dockerimage_select").append($("<option />").val(item.name).text(item.name));
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,65 @@
|
|||
<form method="POST">
|
||||
<div class="form-group">
|
||||
<label>
|
||||
Name<br>
|
||||
<small class="form-text text-muted">Challenge Name</small>
|
||||
</label>
|
||||
<input type="text" class="form-control chal-name" name="name" value="{{ challenge.name }}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>
|
||||
Category<br>
|
||||
<small class="form-text text-muted">Challenge Category</small>
|
||||
</label>
|
||||
<input type="text" class="form-control chal-category" name="category" value="{{ challenge.category }}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="DockerImage" id='dockerimage_label'>Docker Image:
|
||||
<i class="far fa-question-circle text-muted cursor-help" data-toggle="tooltip" data-placement="right" title="The docker image for your challenge"></i>
|
||||
</label>
|
||||
<select id="dockerimage_select" name="docker_image" class="form-control" required></select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>
|
||||
Message<br>
|
||||
<small class="form-text text-muted">
|
||||
Use this to give a brief introduction to your challenge.
|
||||
</small>
|
||||
</label>
|
||||
<textarea id="desc-editor" class="form-control chal-desc-editor" name="description" rows="10">{{ challenge.description }}</textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="value">
|
||||
Value<br>
|
||||
<small class="form-text text-muted">
|
||||
This is how many points teams will receive once they solve this challenge.
|
||||
</small>
|
||||
</label>
|
||||
<input type="number" class="form-control chal-value" name="value" value="{{ challenge.value }}" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>
|
||||
Max Attempts<br>
|
||||
<small class="form-text text-muted">Maximum amount of attempts users receive. Leave at 0 for unlimited.</small>
|
||||
</label>
|
||||
<input type="number" class="form-control chal-attempts" name="max_attempts" value="{{ challenge.max_attempts }}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>
|
||||
State<br>
|
||||
<small class="form-text text-muted">Changes the state of the challenge (e.g. visible, hidden)</small>
|
||||
</label>
|
||||
<select class="form-control" name="state">
|
||||
<option value="visible" {% if challenge.state=="visible" %}selected{% endif %}>Visible</option>
|
||||
<option value="hidden" {% if challenge.state=="hidden" %}selected{% endif %}>Hidden</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<button class="btn btn-success btn-outlined float-right" type="submit">
|
||||
Update
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<script>
|
||||
var DOCKER_IMAGE = '{{ challenge.docker_image }}';
|
||||
</script>
|
|
@ -0,0 +1,12 @@
|
|||
CTFd.plugin.run((_CTFd) => {
|
||||
const $ = _CTFd.lib.$
|
||||
const md = _CTFd.lib.markdown()
|
||||
$(document).ready(function() {
|
||||
$.getJSON("/api/v1/docker", function(result) {
|
||||
$.each(result['data'], function(i, item) {
|
||||
$("#dockerimage_select").append($("<option />").val(item.name).text(item.name));
|
||||
});
|
||||
$("#dockerimage_select").val(DOCKER_IMAGE).change();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,127 @@
|
|||
<script>
|
||||
var DOCKER_CONTAINER = '{{ docker_image | safe }}';
|
||||
(function() {
|
||||
get_docker_status(DOCKER_CONTAINER);
|
||||
})();
|
||||
</script>
|
||||
<script src='/plugins/docker_challenges/assets/ezq.js'></script>
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-body">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" href="#challenge">Challenge</a>
|
||||
</li>
|
||||
{% if solves == None %}
|
||||
{% else %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link challenge-solves" href="#solves">
|
||||
{{ solves }} {% if solves > 1 %}Solves{% else %}Solves{% endif %}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
<div role="tabpanel">
|
||||
<div class="tab-content">
|
||||
<div role="tabpanel" class="tab-pane fade show active" id="challenge">
|
||||
<h2 class='challenge-name text-center pt-3'>{{ name }}</h2>
|
||||
<h3 class="challenge-value text-center">{{ value }}</h3>
|
||||
<div class="challenge-tags text-center">
|
||||
{% for tag in tags %}
|
||||
<span class='badge badge-info challenge-tag'>{{ tag }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<span class="challenge-desc">{{ description | safe }}</span>
|
||||
<div class='mb-3 text-center' id='docker_container'>
|
||||
<span>
|
||||
<a onclick="start_container('{{ docker_image | safe }}');" class='btn btn-dark'>
|
||||
<small style='color:white;'><i class="fas fa-play"></i> Start Docker Instance</small>
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
<div class="challenge-hints hint-row row">
|
||||
{% for hint in hints %}
|
||||
<div class='col-md-12 hint-button-wrapper text-center mb-3'>
|
||||
<a class="btn btn-info btn-hint btn-block load-hint" href="javascript:;" data-hint-id="{{ hint.id }}">
|
||||
{% if hint.hint %}
|
||||
<small>
|
||||
View Hint
|
||||
</small>
|
||||
{% else %}
|
||||
{% if hint.cost %}
|
||||
<small>
|
||||
Unlock Hint for {{ hint.cost }} points
|
||||
</small>
|
||||
{% else %}
|
||||
<small>
|
||||
View Hint
|
||||
</small>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="row challenge-files text-center pb-3">
|
||||
{% for file in files %}
|
||||
<div class='col-md-4 col-sm-4 col-xs-12 file-button-wrapper d-block'>
|
||||
<a class='btn btn-info btn-file mb-1 d-inline-block px-2 w-100 text-truncate' href='{{ file }}'>
|
||||
<i class="fas fa-download"></i>
|
||||
<small>
|
||||
{% set segments = file.split('/') %}
|
||||
{% set file = segments | last %}
|
||||
{% set token = file.split('?') | last %}
|
||||
{% if token %}
|
||||
{{ file | replace("?" + token, "") }}
|
||||
{% else %}
|
||||
{{ file }}
|
||||
{% endif %}
|
||||
</small>
|
||||
</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="row submit-row">
|
||||
<div class="col-md-9 form-group">
|
||||
<input class="form-control" type="text" name="answer" id="submission-input" placeholder="Flag" />
|
||||
<input id="challenge-id" type="hidden" value="{{ id }}">
|
||||
</div>
|
||||
<div class="col-md-3 form-group key-submit">
|
||||
<button type="submit" id="submit-key" tabindex="5" class="btn btn-md btn-outline-secondary float-right">Submit
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row notification-row">
|
||||
<div class="col-md-12">
|
||||
<div id="result-notification" class="alert alert-dismissable text-center w-100" role="alert" style="display: none;">
|
||||
<strong id="result-message"></strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane fade" id="solves">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<table class="table table-striped text-center">
|
||||
<thead>
|
||||
<tr>
|
||||
<td><b>Name</b>
|
||||
</td>
|
||||
<td><b>Date</b>
|
||||
</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="challenge-solves-names">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,125 @@
|
|||
CTFd._internal.challenge.data = undefined
|
||||
|
||||
CTFd._internal.challenge.renderer = CTFd.lib.markdown();
|
||||
|
||||
|
||||
CTFd._internal.challenge.preRender = function() {}
|
||||
|
||||
CTFd._internal.challenge.render = function(markdown) {
|
||||
|
||||
return CTFd._internal.challenge.renderer.render(markdown)
|
||||
}
|
||||
|
||||
|
||||
CTFd._internal.challenge.postRender = function() {}
|
||||
|
||||
|
||||
CTFd._internal.challenge.submit = function(preview) {
|
||||
var challenge_id = parseInt(CTFd.lib.$('#challenge-id').val())
|
||||
var submission = CTFd.lib.$('#submission-input').val()
|
||||
|
||||
var body = {
|
||||
'challenge_id': challenge_id,
|
||||
'submission': submission,
|
||||
}
|
||||
var params = {}
|
||||
if (preview) {
|
||||
params['preview'] = true
|
||||
}
|
||||
|
||||
return CTFd.api.post_challenge_attempt(params, body).then(function(response) {
|
||||
if (response.status === 429) {
|
||||
// User was ratelimited but process response
|
||||
return response
|
||||
}
|
||||
if (response.status === 403) {
|
||||
// User is not logged in or CTF is paused.
|
||||
return response
|
||||
}
|
||||
return response
|
||||
})
|
||||
};
|
||||
|
||||
function get_docker_status(container) {
|
||||
$.get("/api/v1/docker_status", function(result) {
|
||||
$.each(result['data'], function(i, item) {
|
||||
if (item.docker_image == container) {
|
||||
var ports = String(item.ports).split(',');
|
||||
var data = '';
|
||||
$.each(ports, function(x, port) {
|
||||
port = String(port)
|
||||
data = data + 'Host: ' + item.host + ' Port: ' + port + '<br />';
|
||||
})
|
||||
$('#docker_container').html('<pre>Docker Container Information:<br />' + data + '<div class="mt-2" id="' + String(item.instance_id).substring(0,10) + '_revert_container"></div>');
|
||||
var countDownDate = new Date(parseInt(item.revert_time) * 1000).getTime();
|
||||
var x = setInterval(function() {
|
||||
var now = new Date().getTime();
|
||||
var distance = countDownDate - now;
|
||||
var minutes = Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60));
|
||||
var seconds = Math.floor((distance % (1000 * 60)) / 1000);
|
||||
if (seconds < 10) {
|
||||
seconds = "0" + seconds
|
||||
}
|
||||
$("#" + String(item.instance_id).substring(0,10) + "_revert_container").html('Next Revert Available in ' + minutes + ':' + seconds);
|
||||
if (distance < 0) {
|
||||
clearInterval(x);
|
||||
$("#" + String(item.instance_id).substring(0,10) + "_revert_container").html('<a onclick="start_container(\'' + item.docker_image + '\');" class=\'btn btn-dark\'><small style=\'color:white;\'><i class="fas fa-redo"></i> Revert</small></a>');
|
||||
}
|
||||
}, 1000);
|
||||
return false;
|
||||
};
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
function start_container(container) {
|
||||
$('#docker_container').html('<div class="text-center"><i class="fas fa-circle-notch fa-spin fa-1x"></i></div>');
|
||||
$.get("/api/v1/container", { 'name': container }, function(result) {
|
||||
get_docker_status(container);
|
||||
})
|
||||
.fail(function(jqxhr, settings, ex) {
|
||||
ezal({
|
||||
title: "Attention!",
|
||||
body: "You can only revert a container once per 5 minutes! Please be patient.",
|
||||
button: "Got it!"
|
||||
});
|
||||
$(get_docker_status(container));
|
||||
});
|
||||
}
|
||||
|
||||
var modal =
|
||||
'<div class="modal fade" tabindex="-1" role="dialog">' +
|
||||
' <div class="modal-dialog" role="document">' +
|
||||
' <div class="modal-content">' +
|
||||
' <div class="modal-header">' +
|
||||
' <h5 class="modal-title">{0}</h5>' +
|
||||
' <button type="button" class="close" data-dismiss="modal" aria-label="Close">' +
|
||||
' <span aria-hidden="true">×</span>' +
|
||||
" </button>" +
|
||||
" </div>" +
|
||||
' <div class="modal-body">' +
|
||||
" <p>{1}</p>" +
|
||||
" </div>" +
|
||||
' <div class="modal-footer">' +
|
||||
" </div>" +
|
||||
" </div>" +
|
||||
" </div>" +
|
||||
"</div>";
|
||||
|
||||
function ezal(args) {
|
||||
var res = modal.format(args.title, args.body);
|
||||
var obj = $(res);
|
||||
var button = '<button type="button" class="btn btn-primary" data-dismiss="modal">{0}</button>'.format(
|
||||
args.button
|
||||
);
|
||||
obj.find(".modal-footer").append(button);
|
||||
$("main").append(obj);
|
||||
|
||||
obj.modal("show");
|
||||
|
||||
$(obj).on("hidden.bs.modal", function(e) {
|
||||
$(this).modal("dispose");
|
||||
});
|
||||
|
||||
return obj;
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
[
|
||||
{
|
||||
"name": "Docker Config",
|
||||
"route": "/admin/docker_config"
|
||||
},
|
||||
{
|
||||
"name": "Docker Status",
|
||||
"route": "/admin/docker_status"
|
||||
}
|
||||
]
|
|
@ -0,0 +1,191 @@
|
|||
{% extends 'admin/base.html' %}
|
||||
{% block content %}
|
||||
<div class="jumbotron">
|
||||
<div class="container">
|
||||
<h1>Docker Status</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-6 offset-md-3">
|
||||
{% for error in errors %}
|
||||
<div class="alert alert-danger alert-dismissable" role="alert">
|
||||
<span class="sr-only">Error:</span>
|
||||
{{ error }}
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% if dockers %}
|
||||
<table id='dockers' class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="10px" class="text-center" style="cursor: pointer;" onclick="sortTable(0)">ID</th>
|
||||
<th class="text-left" style="cursor: pointer;" onclick="sortTable(1)">{% if dockers[0].team_id %}Team{% else %}User{% endif %}</th>
|
||||
<th class="text-left" style="cursor: pointer;" onclick="sortTable(2)">Docker Image</th>
|
||||
<th class="text-left" style="cursor: pointer;" onclick="sortTable(3)">Instance ID</th>
|
||||
<th class="text-left">Revoke</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for docker in dockers %}
|
||||
<tr id='tr_{{docker.instance_id}}' name='{{docker.id}}'>
|
||||
<td class='text-center' value='{{docker.id}}'>{{docker.id}}</td>
|
||||
{% if docker.team_id %}
|
||||
<td class='text-center' value='{{docker.team_id | safe }}'>{{docker.team_id | safe }}</td>
|
||||
{% else %}
|
||||
<td class='text-center' value='{{docker.team_id | safe }}'>{{docker.user_id | safe }}</td>
|
||||
{% endif %}
|
||||
<td class='text-center' value='{{docker.docker_image}}'>{{docker.docker_image}}</td>
|
||||
<td class='text-center' value='{{docker.instance_id | truncate(15)}}'>{{docker.instance_id | truncate(15)}}</td>
|
||||
<td class='text-center'><a id="delete_{{docker.instance_id}}" style="cursor: pointer;" class="fas fa-trash" onclick="check_nuke_container('{{docker.instance_id}}', false)"></a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class='text-center'>
|
||||
<button type="button" class="btn btn-danger" onclick="check_nuke_container(null, true)">Nuke All Containers</button>
|
||||
</div>
|
||||
{% else %}
|
||||
<h3 class='text-center'> No Docker Containers Active</h3>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
{% block scripts %}
|
||||
<script>
|
||||
function sortTable(n) {
|
||||
var table, rows, switching, i, x, y, shouldSwitch, dir, switchcount = 0;
|
||||
table = document.getElementById("dockers");
|
||||
switching = true;
|
||||
dir = "asc";
|
||||
while (switching) {
|
||||
switching = false;
|
||||
rows = table.rows;
|
||||
for (i = 1; i < (rows.length - 1); i++) {
|
||||
shouldSwitch = false;
|
||||
x = rows[i].getElementsByTagName("TD")[n];
|
||||
y = rows[i + 1].getElementsByTagName("TD")[n];
|
||||
if (dir == "asc") {
|
||||
if (x.innerHTML.toLowerCase() > y.innerHTML.toLowerCase()) {
|
||||
shouldSwitch = true;
|
||||
break;
|
||||
}
|
||||
} else if (dir == "desc") {
|
||||
if (x.innerHTML.toLowerCase() < y.innerHTML.toLowerCase()) {
|
||||
shouldSwitch = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (shouldSwitch) {
|
||||
rows[i].parentNode.insertBefore(rows[i + 1], rows[i]);
|
||||
switching = true;
|
||||
switchcount++;
|
||||
} else {
|
||||
if (switchcount == 0 && dir == "asc") {
|
||||
dir = "desc";
|
||||
switching = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<script>
|
||||
function check_nuke_container(instance, all) {
|
||||
ezq({
|
||||
title: "Attention!",
|
||||
body: "Are You Sure You want to do this?",
|
||||
success: function() { nuke_container(instance, all) },
|
||||
});
|
||||
}
|
||||
|
||||
function nuke_container(instance, all) {
|
||||
var xhttp = new XMLHttpRequest();
|
||||
xhttp.onreadystatechange = function() {
|
||||
if (this.readyState == 4 && this.status == 200) {
|
||||
if (all == true) {
|
||||
window.location = '{{ script_root }}/admin/docker_status'
|
||||
}
|
||||
else {
|
||||
document.getElementById("tr_" + instance).style.display = "none";
|
||||
}
|
||||
} else if (this.readyState == 4 && this.status != 200) {
|
||||
ezal({
|
||||
title: "Attention!",
|
||||
body: "Error when Deleting Docker Container",
|
||||
button: "Got it!",
|
||||
});
|
||||
}
|
||||
};
|
||||
xhttp.open("GET", "/api/v1/nuke?container=" + instance + "&all=" + all, true);
|
||||
xhttp.send();
|
||||
}
|
||||
</script>
|
||||
<script>
|
||||
var modal =
|
||||
'<div class="modal fade" tabindex="-1" role="dialog">' +
|
||||
' <div class="modal-dialog" role="document">' +
|
||||
' <div class="modal-content">' +
|
||||
' <div class="modal-header">' +
|
||||
' <h5 class="modal-title">{0}</h5>' +
|
||||
' <button type="button" class="close" data-dismiss="modal" aria-label="Close">' +
|
||||
' <span aria-hidden="true">×</span>' +
|
||||
" </button>" +
|
||||
" </div>" +
|
||||
' <div class="modal-body">' +
|
||||
" <p>{1}</p>" +
|
||||
" </div>" +
|
||||
' <div class="modal-footer">' +
|
||||
" </div>" +
|
||||
" </div>" +
|
||||
" </div>" +
|
||||
"</div>";
|
||||
|
||||
function ezq(args) {
|
||||
var res = modal.format(args.title, args.body);
|
||||
var obj = $(res);
|
||||
var deny =
|
||||
'<button type="button" class="btn btn-danger" data-dismiss="modal">No</button>';
|
||||
var confirm = $(
|
||||
'<button type="button" class="btn btn-primary" data-dismiss="modal">Yes</button>'
|
||||
);
|
||||
|
||||
obj.find(".modal-footer").append(deny);
|
||||
obj.find(".modal-footer").append(confirm);
|
||||
|
||||
$("main").append(obj);
|
||||
|
||||
$(obj).on("hidden.bs.modal", function(e) {
|
||||
$(this).modal("dispose");
|
||||
});
|
||||
|
||||
$(confirm).click(function() {
|
||||
args.success();
|
||||
});
|
||||
|
||||
obj.modal("show");
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
function ezal(args) {
|
||||
var res = modal.format(args.title, args.body);
|
||||
var obj = $(res);
|
||||
var button = '<button type="button" class="btn btn-primary" data-dismiss="modal">{0}</button>'.format(
|
||||
args.button
|
||||
);
|
||||
|
||||
obj.find(".modal-footer").append(button);
|
||||
$("main").append(obj);
|
||||
|
||||
obj.modal("show");
|
||||
|
||||
$(obj).on("hidden.bs.modal", function(e) {
|
||||
$(this).modal("dispose");
|
||||
});
|
||||
|
||||
return obj;
|
||||
}
|
||||
</script>
|
||||
{% endblock scripts %}
|
|
@ -0,0 +1,101 @@
|
|||
{% extends 'admin/base.html' %}
|
||||
{% block content %}
|
||||
<div class="jumbotron">
|
||||
<div class="container">
|
||||
<h1>Docker Config</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-6 offset-md-3">
|
||||
{% for error in errors %}
|
||||
<div class="alert alert-danger alert-dismissable" role="alert">
|
||||
<span class="sr-only">Error:</span>
|
||||
{{ error }}
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<form method="post" accept-charset="utf-8" autocomplete="off" role="form" name='docker_config' class="form-horizontal" enctype="multipart/form-data">
|
||||
<div class="form-group">
|
||||
<label for="hostname-input">
|
||||
Hostname
|
||||
</label>
|
||||
{% if config.hostname %}
|
||||
<input class="form-control" type="text" name="hostname" id="hostname-input" placeholder="Ex: 10.10.10.10:2376" value='{{ config.hostname }}'/>
|
||||
{% else %}
|
||||
<input class="form-control" type="text" name="hostname" id="hostname-input" placeholder="Ex: 10.10.10.10:2376" />
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
{% if config.tls_enabled %}
|
||||
<input type="checkbox" name="tls_enabled" id="tls-checkbox" value="True" onclick="enable_file_form(this.checked)" checked />
|
||||
{% else %}
|
||||
<input type="checkbox" name="tls_enabled" id="tls-checkbox" value="False" onclick="enable_file_form(this.checked)" />
|
||||
{% endif %}
|
||||
<label for="tls-checkbox">TLS Enabled?</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="ca-file">
|
||||
CA Cert {% if config.ca_cert %} (Uploaded. Adding a new file will overwrite.) {% endif %}
|
||||
</label>
|
||||
<input class="form-control" type="file" name="ca_cert" id="ca_file" {% if not config.tls_enabled %} disabled {% endif %} {% if config.tls_enabled and not config.ca_cert %} required {% endif %} />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="client-file">
|
||||
Client Cert {% if config.client_cert %} (Uploaded. Adding a new file will overwrite.) {% endif %}
|
||||
</label>
|
||||
<input class="form-control" type="file" name="client_cert" id="client_file" {% if not config.tls_enabled %} disabled {% endif %} {% if config.tls_enabled and not config.client_cert %} required {% endif %} />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="key-file">
|
||||
Client Key {% if config.client_key %} (Uploaded. Adding a new file will overwrite.) {% endif %}
|
||||
</label>
|
||||
<input class="form-control" type="file" name="client_key" id="key_file" {% if not config.tls_enabled %} disabled {% endif %} {% if config.tls_enabled and not config.client_key %} required {% endif %} />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="repo-ms">
|
||||
Repositories
|
||||
</label>
|
||||
<select id='repositories' name="repositories" class='form-control' size='10' multiple>
|
||||
{% if form.repositories.choices[0][0] == "ERROR" %}
|
||||
<option value='False' disabled>{{ form.repositories.choices[0][1] }}</option>
|
||||
{% elif form.repositories %}
|
||||
{% for key,value in form.repositories.choices %}
|
||||
{% if key in repos %}
|
||||
<option value='{{ key }}' selected>{{ value }}</option>
|
||||
{% else %}
|
||||
<option value='{{ key }}'>{{ value }}</option>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<option value='False' disabled>Connect Docker API First</option>
|
||||
{% endif %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-13 text-center">
|
||||
<button type="submit" tabindex="0" class="btn btn-md btn-primary btn-outlined">
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" name="nonce" value="{{ nonce }}">
|
||||
<input type="hidden" name="id" value="1">
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
{% block scripts %}
|
||||
<script>
|
||||
|
||||
function enable_file_form(status) {
|
||||
status=!status;
|
||||
document.docker_config.ca_file.disabled = status;
|
||||
document.docker_config.client_file.disabled = status;
|
||||
document.docker_config.key_file.disabled = status;
|
||||
document.docker_config.ca_file.required = !status;
|
||||
document.docker_config.client_file.required = !status;
|
||||
document.docker_config.key_file.required = !status;
|
||||
}
|
||||
|
||||
</script>
|
||||
{% endblock scripts %}
|
Loading…
Reference in New Issue