Fork project

This commit is contained in:
2025-09-19 15:59:08 +08:00
commit 2f921b6209
52 changed files with 4012 additions and 0 deletions

View File

@ -0,0 +1,24 @@
<div class="tab-pane fade" id="router" role="tabpanel">
{% set value = get_config('whale:router_type') %}
{% set cur_type = get_config("whale:router_type", "frp") %}
<div class="form-group">
<label for="router-type">
Router type
<small class="form-text text-muted">
Select which router backend to use
</small>
</label>
<select id="router-type" class="form-control custom-select" onchange="window.updateConfigs">
{% for type in ["frp", "trp"] %}
<option value="{{ type }}" {{ "selected" if value == type }}>{{ type }}</option>
{% endfor %}
</select>
</div>
{% set template = "config/" + cur_type + ".router.config.html" %}
{% include template %}
<div class="submit-row float-right">
<button type="submit" tabindex="0" class="btn btn-md btn-primary btn-outlined">
Submit
</button>
</div>
</div>

View File

@ -0,0 +1,25 @@
<div class="tab-pane fade" id="challenges" role="tabpanel">
{% for config, val in {
"Subdomain Template": ("template_http_subdomain", "Controls how the subdomain of a container is generated"),
"Flag Template": ("template_chall_flag", "Controls how a flag is generated"),
}.items() %}
{% set value = get_config('whale:' + val[0]) %}
<div class="form-group">
<label for="{{ val[0].replace('_', '-') }}">
{{ config }}
<small class="form-text text-muted">
{{ val[1] }}
</small>
</label>
<input type="text" class="form-control"
id="{{ val[0].replace('_', '-') }}" name="{{ 'whale:' + val[0] }}"
{% if value != None %}value="{{ value }}"{% endif %}>
</div>
{% endfor %}
<div class="submit-row float-right">
<button type="submit" tabindex="0" class="btn btn-md btn-primary btn-outlined">
Submit
</button>
</div>
</div>

View File

@ -0,0 +1,122 @@
<div class="tab-pane fade show active" id="docker" role="tabpanel" aria-autocomplete="none">
<h5>Common</h5>
<small class="form-text text-muted">
Common configurations for both standalone and grouped containers
</small><br>
{% for config, val in {
"API URL": ("docker_api_url", "Docker API to connect to"),
"Credentials": ("docker_credentials", "docker.io username and password, separated by ':'. useful for private images"),
"Swarm Nodes": ("docker_swarm_nodes", "Will pick up one from it, You should set your node with label name=windows-* or name=linux-*. Separated by commas."),
}.items() %}
{% set value = get_config('whale:' + val[0]) %}
<div class="form-group">
<label for="{{ val[0].replace('_', '-') }}">
{{ config }}
<small class="form-text text-muted">{{ val[1] }}</small>
</label>
<input type="text" class="form-control"
id="{{ val[0].replace('_', '-') }}" name="{{ 'whale:' + val[0] }}"
{% if value != None %}value="{{ value }}"{% endif %}>
</div>
{% endfor %}
{% set use_ssl = get_config('whale:docker_use_ssl') %}
<div class="form-check">
<input type="checkbox" id="docker-use-ssl" name="whale:docker_use_ssl"
{% if use_ssl == True %}checked{% endif %}>
<label for="docker-use-ssl">Use SSL</label>
</div>
<div class="container" id="docker-ssl-config">
<div class="form-group">
<label for="docker-ssl-ca-cert">
SSL CA Certificate
<small class="form-text text-muted">
the location of the CA certificate file used in ssl connection
</small>
</label>
<input type="text" class="form-control"
id="docker-ssl-ca-cert" name="whale:docker_ssl_ca_cert"
value="{{ get_config('whale:docker_ssl_ca_cert') }}">
</div>
<div class="form-group">
<label for="docker-ssl-client-cert">
SSL Client Certificate
<small class="form-text text-muted">
the location of the client certificate file used in ssl connection
</small>
</label>
<input type="text" class="form-control"
id="docker-ssl-client-cert" name="whale:docker_ssl_client_cert"
value="{{ get_config('whale:docker_ssl_client_cert') }}">
</div>
<div class="form-group">
<label for="docker-ssl-client-key">
SSL Client Key
<small class="form-text text-muted">
the location of the client key file used in ssl connection
</small>
</label>
<input type="text" class="form-control"
id="docker-ssl-client-key" name="whale:docker_ssl_client_key"
value="{{ get_config('whale:docker_ssl_client_key') }}">
</div>
</div>
<script>
(function () {
let config = document.getElementById('docker-ssl-config');
let option = document.getElementById('docker-use-ssl');
config.hidden = !option.checked;
option.onclick = () => (config.hidden = !option.checked) || true;
}) ()
</script>
<hr>
<h5>Standalone Containers</h5>
<small class="form-text text-muted">
Typical challenges. Under most circumstances you only need to set these.
</small><br>
{% for config, val in {
"Auto Connect Network": ("docker_auto_connect_network", "The network connected for single-containers. It's usually the same network as the frpc is in."),
"Dns Setting": ("docker_dns", "Decide which dns will be used in container network."),
}.items() %}
{% set value = get_config('whale:' + val[0]) %}
<div class="form-group">
<label for="{{ val[0].replace('_', '-') }}">
{{ config }}
<small class="form-text text-muted">
{{ val[1] }}
</small>
</label>
<input type="text" class="form-control"
id="{{ val[0].replace('_', '-') }}" name="{{ 'whale:' + val[0] }}"
{% if value != None %}value="{{ value }}"{% endif %}>
</div>
{% endfor %}
<hr>
<h5>Grouped Containers</h5>
<small class="form-text text-muted">
Designed for multi-container challenges
</small><br>
{% for config, val in {
"Auto Connect Containers": ("docker_auto_connect_containers","Decide which container will be connected to multi-container-network automatically. Separated by commas."),
"Multi-Container Network Subnet": ("docker_subnet", "Subnet which will be used by auto created networks for multi-container challenges."),
"Multi-Container Network Subnet New Prefix": ("docker_subnet_new_prefix", "Prefix for auto created network.")
}.items() %}
{% set value = get_config('whale:' + val[0]) %}
<div class="form-group">
<label for="{{ val[0].replace('_', '-') }}">
{{ config }}
<small class="form-text text-muted">
{{ val[1] }}
</small>
</label>
<input type="text" class="form-control"
id="{{ val[0].replace('_', '-') }}" name="{{ 'whale:' + val[0] }}"
{% if value != None %}value="{{ value }}"{% endif %}>
</div>
{% endfor %}
<div class="submit-row float-right">
<button type="submit" tabindex="0" class="btn btn-md btn-primary btn-outlined">
Submit
</button>
</div>
</div>

View File

@ -0,0 +1,50 @@
{% for config, val in {
"API URL": ("frp_api_url", "Frp API to connect to"),
"Http Domain Suffix": ("frp_http_domain_suffix", "Will be appended to the hash of a container"),
"External Http Port": ("frp_http_port", "Keep in sync with frps:vhost_http_port"),
"Direct IP Address":("frp_direct_ip_address","For direct redirect"),
"Direct Minimum Port": ("frp_direct_port_minimum", "For direct redirect (pwn challenges)"),
"Direct Maximum Port": ("frp_direct_port_maximum", "For direct redirect (pwn challenges)"),
}.items() %}
{% set value = get_config('whale:' + val[0]) %}
<div class="form-group">
<label for="{{ val[0].replace('_', '-') }}">
{{ config }}
<small class="form-text text-muted">
{{ val[1] }}
</small>
</label>
<input type="text" class="form-control" id="{{ val[0].replace('_', '-') }}" name="{{ 'whale:' + val[0] }}" {% if
value !=None %}value="{{ value }}" {% endif %}>
</div>
{% endfor %}
{% set frpc_template = get_config("whale:frp_config_template", "") %}
<div class="form-group">
<label for="frp-config-template">
Frpc config template
<small class="form-text text-muted">
Frp config template, only need common section!
</small>
</label>
<textarea class="form-control input-filled-valid" id="frp-config-template" rows="7"
name="whale:frp_config_template">{{ frpc_template }}</textarea>
</div>
{% if frpc_template %}
<div class="form-group">
<label for="frps-config-template">
Frps config template [generated]
<small class="form-text text-muted">
This configuration is generated with your settings above.
</small>
</label>
<textarea class="form-control input-filled-valid grey-text" id="frps-config-template" rows="6" disabled>
[common]
{% for i in frpc_template.split('\n') %}
{%- if 'token' in i -%}{{ i }}{%- endif -%}
{%- if 'server_port' in i -%}{{ i.replace('server_port', 'bind_port') }}{%- endif -%}
{% endfor %}
vhost_http_port = {{ get_config('whale:frp_http_port') }}
subdomain_host = {{ get_config('whale:frp_http_domain_suffix', '127.0.0.1.xip.io').lstrip('.') }}
</textarea>
</div>
{% endif %}

View File

@ -0,0 +1,26 @@
<div class="tab-pane fade" id="limits" role="tabpanel">
{% for config, val in {
"Max Container Count": ("docker_max_container_count", "The maximum number of countainers allowed on the server"),
"Max Renewal Times": ("docker_max_renew_count", "The maximum times a user is allowed to renew a container"),
"Docker Container Timeout": ("docker_timeout", "A container times out after [timeout] seconds."),
}.items() %}
{% set value = get_config('whale:' + val[0]) %}
<div class="form-group">
<label for="{{ val[0].replace('_', '-') }}">
{{ config }}
<small class="form-text text-muted">
{{ val[1] }}
</small>
</label>
<input type="text" class="form-control"
id="{{ val[0].replace('_', '-') }}" name="{{ 'whale:' + val[0] }}"
{% if value != None %}value="{{ value }}"{% endif %}>
</div>
{% endfor %}
<div class="submit-row float-right">
<button type="submit" tabindex="0" class="btn btn-md btn-primary btn-outlined">
Submit
</button>
</div>
</div>

View File

@ -0,0 +1,17 @@
{% for config, val in {
"API URL": ("trp_api_url", "trp API to connect to"),
"Domain Suffix": ("trp_domain_suffix", "Will be used to generated the access link of a challenge"),
"Listening Port": ("trp_listening_port", "Will be used to generated the access link of a challenge"),
}.items() %}
{% set value = get_config('whale:' + val[0]) %}
<div class="form-group">
<label for="{{ val[0].replace('_', '-') }}">
{{ config }}
<small class="form-text text-muted">
{{ val[1] }}
</small>
</label>
<input type="text" class="form-control" id="{{ val[0].replace('_', '-') }}" name="{{ 'whale:' + val[0] }}"
{% if value != None %}value="{{ value }}" {% endif %}>
</div>
{% endfor %}

View File

@ -0,0 +1,57 @@
<style>
.info-card.card {
height: 11rem;
}
.card-text {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.card-text:hover {
white-space: pre-line;
overflow: visible;
}
</style>
<div class="row">
{% for container in containers %}
<div class="col-sm-6 pb-3">
<div class="info-card card">
<div class="card-body">
<h5 class="d-inline-block card-title">
<a style="width: 5rem;"
href="{{ url_for('admin.challenges_detail', challenge_id=container.challenge.id) }}"
>{{ container.challenge.name | truncate(15) }}
</a>
</h5>
<h6 class="d-inline-block card-subtitle float-right">
<a style="width: 5rem;"
class="btn btn-outline-secondary rounded"
href="{{ url_for('admin.users_detail', user_id=container.user.id) }}"
>{{ container.user.name | truncate(5) }}
</a>
</h6>
<p class="card-text">{{ container.user_access }}</p>
<p class="card-text">{{ container.flag }}</p>
Time Started: {{ container.start_time }}
<a class="delete-container float-right" container-id="{{ container.id }}"
data-toggle="tooltip" data-placement="top"
user-id="{{ container.user.id }}"
style="margin-right: 0.5rem;"
title="Destroy Container #{{ container.id }}">
<i class="fas fa-stop-circle"></i>
</a>
<a class="renew-container float-right" container-id="{{ container.id }}"
data-toggle="tooltip" data-placement="top"
user-id="{{ container.user.id }}"
style="margin-right: 0.5rem;"
title="Renew Container #{{ container.id }}">
<i class="fas fa-clock"></i>
</a>
</div>
</div>
</div>
{% endfor %}
</div>

View File

@ -0,0 +1,78 @@
<div class="row">
<div class="col-md-12">
<table class="table table-striped border">
<thead>
<tr>
<th class="border-right" data-checkbox>
<div class="form-check text-center">&nbsp;
<input type="checkbox" class="form-check-input" data-checkbox-all>
</div>
</th>
<th class="sort-col text-center"><b>ID</b></td>
<th class="text-center"><b>User</b></td>
<th class="sort-col text-center"><b>Challenge</b></td>
<th class="text-center"><b>Access Method</b></td>
<th class="text-center"><b>Flag</b></td>
<th class="sort-col text-center"><b>Startup Time</b></td>
<th class="sort-col text-center"><b>Renewal Times</b></td>
<th class="text-center"><b>Delete</b></td>
</tr>
</thead>
<tbody>
{% for container in containers %}
<tr>
<td class="border-right" data-checkbox>
<div class="form-check text-center">&nbsp;
<input type="checkbox" class="form-check-input" data-user-id="{{ container.user.id }}">
</div>
</td>
<td class="text-center">
{{ container.id }}
</td>
<td class="text-center">
<a href="{{ url_for('admin.users_detail', user_id=container.user.id) }}">
{{ container.user.name | truncate(12) }}
</a>
</td>
<td class="text-center">
<a href="{{ url_for('admin.challenges_detail', challenge_id=container.challenge.id) }}">
{{ container.challenge.name }}
</a>
</td>
<td class="text-center">
{{ container.challenge.redirect_type }}&nbsp;
<button class="btn btn-link p-0 click-copy" data-copy="{{ container.user_access }}">
<i class="fas fa-clipboard"></i>
</button>
</td>
<td class="text-center">
<button class="btn btn-link p-0 click-copy" data-copy="{{ container.flag }}">
<i class="fas fa-clipboard"></i>
</button>
</td>
<td class="text-center">
<span data-time="{{ container.start_time | isoformat }}"></span>
</td>
<td class="text-center">
{{ container.renew_count }}&nbsp;
<button class="btn btn-link p-0 renew-container"
container-id="{{ container.id }}" data-toggle="tooltip"
user-id="{{ container.user.id }}" data-placement="top"
title="Renew Container #{{ container.id }}">
<i class="fas fa-sync"></i>
</button>
</td>
<td class="text-center">
<button class="btn btn-link p-0 delete-container"
container-id="{{ container.id }}" data-toggle="tooltip"
user-id="{{ container.user.id }}" data-placement="top"
title="Destroy Container #{{ container.id }}">
<i class="fas fa-times"></i>
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>

25
templates/whale_base.html Normal file
View File

@ -0,0 +1,25 @@
{% extends "admin/base.html" %}
{% block content %}
<div class="jumbotron">
<div class="container">
<h1>CTFd Whale</h1>
</div>
</div>
<div class="container">
<div class="row">
<div class="col-md-3">
<ul class="nav nav-pills flex-column">
{% block menu %}
{% endblock %}
</ul>
</div>
<div class="col-md-9">
<div class="tab-content">
{% block panel %}
{% endblock %}
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,38 @@
{% extends "whale_base.html" %}
{% block menu %}
<li class="nav-item">
<a class="nav-link active" data-toggle="pill" href="#docker">Docker</a>
</li>
<li class="nav-item">
<a class="nav-link" data-toggle="pill" href="#router">Router</a>
</li>
<li class="nav-item">
<a class="nav-link" data-toggle="pill" href="#limits">Limits</a>
</li>
<li class="nav-item">
<a class="nav-link" data-toggle="pill" href="#challenges">Challenges</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/plugins/ctfd-whale/admin/containers">🔗 Instances</a>
</li>
{% endblock %}
{% block panel %}
{% include "components/errors.html" %}
<div role="tabpanel" class="tab-pane config-section active" id="settings">
<form method="POST" accept-charset="utf-8" action="/admin/plugins/ctfd-whale"
class="form-horizontal">
<div class="tab-content">
{% include "config/docker.config.html" %}
{% include "config/base.router.config.html" %}
{% include "config/limits.config.html" %}
{% include "config/challenges.config.html" %}
</div>
</form>
</div>
{% endblock %}
{% block scripts %}
<script defer src="{{ url_for('plugins.ctfd-whale.assets', path='config.js') }}"></script>
{% endblock %}

View File

@ -0,0 +1,69 @@
{% extends "whale_base.html" %}
{% block menu %}
<li class="nav-item">
<a class="nav-link" href="/plugins/ctfd-whale/admin/settings">🔗 Settings</a>
</li>
<li class="nav-item">
<a class="nav-link active" href="#">Instances</a>
</li>
<li class="nav-item nav-link">
<div class="btn-group" role="group">
<button type="button" class="btn btn-outline-secondary"
data-toggle="tooltip" title="Renew Containers" id="containers-renew-button">
<i class="btn-fa fas fa-sync"></i>
</button>
<button type="button" class="btn btn-outline-danger"
data-toggle="tooltip" title="Stop Containers" id="containers-delete-button">
<i class="btn-fa fas fa-times"></i>
</button>
</div>
</li>
<li class="nav-item nav-link">
<ul class="pagination">
<li class="page-item{{ ' disabled' if curr_page <= 1 else '' }}">
<a class="page-link" aria-label="Previous"
href="/plugins/ctfd-whale/admin/containers?page={{ curr_page - 1 }}"
>
<span aria-hidden="true">&laquo;</span>
<span class="sr-only">Previous</span>
</a>
</li>
{% set range_l = [[curr_page - 1, 1]|max, [pages - 3, 1]|max]|min %}
{% set range_r = [[curr_page + 2, pages]|min, [4, pages]|min]|max %}
{% for page in range(range_l, range_r + 1) %}
<li class="page-item{{ ' active' if curr_page == page }}">
<a class="page-link"
href="/plugins/ctfd-whale/admin/containers?page={{ page }}"
>{{ page }}</a>
</li>
{% endfor %}
<li class="page-item{{ ' disabled' if curr_page >= pages else '' }}">
<a class="page-link" aria-label="Next"
href="/plugins/ctfd-whale/admin/containers?page={{ curr_page + 1 }}"
>
<span aria-hidden="true">&raquo;</span>
<span class="sr-only">Next</span>
</a>
</li>
</ul>
</li>
<li class="nav-item nav-link">
{% if session['view_mode'] == 'card' %}
<a href="?mode=list">Switch to list mode</a>
{% else %}
<a href="?mode=card">Switch to card mode</a>
{% endif %}
</li>
{% endblock %}
{% block panel %}
{% include "containers/" + session["view_mode"] + ".containers.html" %}
{% endblock %}
{% block scripts %}
<script defer src="{{ url_for('plugins.ctfd-whale.assets', path='containers.js') }}"></script>
{% endblock %}