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

27
assets/config.js Normal file
View File

@ -0,0 +1,27 @@
const $ = CTFd.lib.$;
$(".config-section > form:not(.form-upload)").submit(async function (event) {
event.preventDefault();
const obj = $(this).serializeJSON();
const params = {};
for (let x in obj) {
if (obj[x] === "true") {
params[x] = true;
} else if (obj[x] === "false") {
params[x] = false;
} else {
params[x] = obj[x];
}
}
params['whale:refresh'] = btoa(+new Date).slice(-7, -2);
await CTFd.api.patch_config_list({}, params);
location.reload();
});
$(".config-section > form:not(.form-upload) > div > div > div > #router-type").change(async function () {
await CTFd.api.patch_config_list({}, {
'whale:router_type': $(this).val(),
'whale:refresh': btoa(+new Date).slice(-7, -2),
});
location.reload();
});

120
assets/containers.js Normal file
View File

@ -0,0 +1,120 @@
const $ = CTFd.lib.$;
function htmlentities(str) {
return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
function copyToClipboard(event, str) {
// Select element
const el = document.createElement('textarea');
el.value = str;
el.setAttribute('readonly', '');
el.style.position = 'absolute';
el.style.left = '-9999px';
document.body.appendChild(el);
el.select();
document.execCommand('copy');
document.body.removeChild(el);
$(event.target).tooltip({
title: "Copied!",
trigger: "manual"
});
$(event.target).tooltip("show");
setTimeout(function () {
$(event.target).tooltip("hide");
}, 1500);
}
$(".click-copy").click(function (e) {
copyToClipboard(e, $(this).data("copy"));
})
async function delete_container(user_id) {
let response = await CTFd.fetch("/api/v1/plugins/ctfd-whale/admin/container?user_id=" + user_id, {
method: "DELETE",
credentials: "same-origin",
headers: {
Accept: "application/json",
"Content-Type": "application/json"
}
});
response = await response.json();
return response.success;
}
async function renew_container(user_id) {
let response = await CTFd.fetch(
"/api/v1/plugins/ctfd-whale/admin/container?user_id=" + user_id, {
method: "PATCH",
credentials: "same-origin",
headers: {
Accept: "application/json",
"Content-Type": "application/json"
}
});
response = await response.json();
return response.success;
}
$('#containers-renew-button').click(function (e) {
let users = $("input[data-user-id]:checked").map(function () {
return $(this).data("user-id");
});
CTFd.ui.ezq.ezQuery({
title: "Renew Containers",
body: `Are you sure you want to renew the selected ${users.length} container(s)?`,
success: async function () {
await Promise.all(users.toArray().map((user) => renew_container(user)));
location.reload();
}
});
});
$('#containers-delete-button').click(function (e) {
let users = $("input[data-user-id]:checked").map(function () {
return $(this).data("user-id");
});
CTFd.ui.ezq.ezQuery({
title: "Delete Containers",
body: `Are you sure you want to delete the selected ${users.length} container(s)?`,
success: async function () {
await Promise.all(users.toArray().map((user) => delete_container(user)));
location.reload();
}
});
});
$(".delete-container").click(function (e) {
e.preventDefault();
let container_id = $(this).attr("container-id");
let user_id = $(this).attr("user-id");
CTFd.ui.ezq.ezQuery({
title: "Destroy Container",
body: "<span>Are you sure you want to delete <strong>Container #{0}</strong>?</span>".format(
htmlentities(container_id)
),
success: async function () {
await delete_container(user_id);
location.reload();
}
});
});
$(".renew-container").click(function (e) {
e.preventDefault();
let container_id = $(this).attr("container-id");
let user_id = $(this).attr("user-id");
CTFd.ui.ezq.ezQuery({
title: "Renew Container",
body: "<span>Are you sure you want to renew <strong>Container #{0}</strong>?</span>".format(
htmlentities(container_id)
),
success: async function () {
await renew_container(user_id);
location.reload();
},
});
});

100
assets/create.html Normal file
View File

@ -0,0 +1,100 @@
{% extends "admin/challenges/create.html" %}
{% block header %}
<div class="alert alert-secondary" role="alert">
Dynamic docker challenge allows players to deploy their per-challenge standalone instances.
</div>
{% endblock %}
{% block value %}
<div class="form-group">
<label for="value">Docker Image<br>
<small class="form-text text-muted">
The docker image used to deploy
</small>
</label>
<input type="text" class="form-control" name="docker_image" placeholder="Enter docker image name" required>
</div>
<div class="form-group">
<label for="value">Frp Redirect Type<br>
<small class="form-text text-muted">
Decide the redirect type how frp redirect traffic
</small>
</label>
<select class="form-control" name="redirect_type">
<option value="http" selected>HTTP</option>
<option value="direct">Direct</option>
</select>
</div>
<div class="form-group">
<label for="value">Frp Redirect Port<br>
<small class="form-text text-muted">
Decide which port in the instance that frp should redirect traffic for
</small>
</label>
<input type="number" class="form-control" name="redirect_port" placeholder="Enter the port you want to open"
required>
</div>
<div class="form-group">
<label for="value">Docker Container Memory Limit<br>
<small class="form-text text-muted">
The memory usage limit
</small>
</label>
<input type="text" class="form-control" name="memory_limit" placeholder="Enter the memory limit" value="128m"
required>
</div>
<div class="form-group">
<label for="value">Docker Container CPU Limit<br>
<small class="form-text text-muted">
The CPU usage limit
</small>
</label>
<input type="number" class="form-control" name="cpu_limit" placeholder="Enter the cpu limit" value="0.5"
required>
</div>
<div class="form-group">
<label for="value">Initial Value<br>
<small class="form-text text-muted">
This is how many points the challenge is worth initially.
</small>
</label>
<input type="number" class="form-control" name="value" placeholder="Enter value" required>
</div>
<div class="form-group">
<label for="value">Decay Limit<br>
<small class="form-text text-muted">
The amount of solves before the challenge reaches its minimum value
</small>
</label>
<input type="number" class="form-control" name="decay" placeholder="Enter decay limit" required>
</div>
<div class="form-group">
<label for="value">Minimum Value<br>
<small class="form-text text-muted">
This is the lowest that the challenge can be worth
</small>
</label>
<input type="number" class="form-control" name="minimum" placeholder="Enter minimum value" required>
</div>
<div class="form-group">
<label for="value">Score Type<br>
<small class="form-text text-muted">
Decide it use dynamic score or not
</small>
</label>
<select class="form-control" name="dynamic_score">
<option value="0" selected>Static Score</option>
<option value="1">Dynamic Score</option>
</select>
</div>
{% endblock %}
{% block type %}
<input type="hidden" value="dynamic_docker" name="type" id="chaltype">
{% endblock %}

30
assets/create.js Normal file
View File

@ -0,0 +1,30 @@
// Markdown Preview
if ($ === undefined) $ = CTFd.lib.$
$('#desc-edit').on('shown.bs.tab', function(event) {
if (event.target.hash == '#desc-preview') {
var editor_value = $('#desc-editor').val();
$(event.target.hash).html(
CTFd._internal.challenge.render(editor_value)
);
}
});
$('#new-desc-edit').on('shown.bs.tab', function(event) {
if (event.target.hash == '#new-desc-preview') {
var editor_value = $('#new-desc-editor').val();
$(event.target.hash).html(
CTFd._internal.challenge.render(editor_value)
);
}
});
$("#solve-attempts-checkbox").change(function() {
if (this.checked) {
$('#solve-attempts-input').show();
} else {
$('#solve-attempts-input').hide();
$('#max_attempts').val('');
}
});
$(document).ready(function() {
$('[data-toggle="tooltip"]').tooltip();
});

94
assets/update.html Normal file
View File

@ -0,0 +1,94 @@
{% extends "admin/challenges/update.html" %}
{% block value %}
<div class="form-group">
<label for="value">Current Value<br>
<small class="form-text text-muted">
This is how many points the challenge is worth right now.
</small>
</label>
<input type="number" class="form-control chal-value" name="value" value="{{ challenge.value }}" disabled>
</div>
<div class="form-group">
<label for="value">Initial Value<br>
<small class="form-text text-muted">
This is how many points the challenge was worth initially.
</small>
</label>
<input type="number" class="form-control chal-initial" name="initial" value="{{ challenge.initial }}" required>
</div>
<div class="form-group">
<label for="value">Decay Limit<br>
<small class="form-text text-muted">
The amount of solves before the challenge reaches its minimum value
</small>
</label>
<input type="number" class="form-control chal-decay" name="decay" value="{{ challenge.decay }}" required>
</div>
<div class="form-group">
<label for="value">Minimum Value<br>
<small class="form-text text-muted">
This is the lowest that the challenge can be worth
</small>
</label>
<input type="number" class="form-control chal-minimum" name="minimum" value="{{ challenge.minimum }}" required>
</div>
<div class="form-group">
<label for="value">Docker Image<br>
<small class="form-text text-muted">
The docker image used to deploy
</small>
</label>
<input type="text" class="form-control" name="docker_image" placeholder="Enter docker image name"
required value="{{ challenge.docker_image }}">
</div>
<div class="form-group">
<label for="value">Frp Redirect Type<br>
<small class="form-text text-muted">
Decide the redirect type how frp redirect traffic
</small>
</label>
<select class="form-control" name="redirect_type">
<option value="http" {% if challenge.redirect_type == "http" %}selected{% endif %}>HTTP</option>
<option value="direct" {% if challenge.redirect_type == "direct" %}selected{% endif %}>Direct</option>
</select>
</div>
<div class="form-group">
<label for="value">Frp Redirect Port<br>
<small class="form-text text-muted">
Decide which port in the instance that frp should redirect traffic for
</small>
</label>
<input type="number" class="form-control" name="redirect_port" placeholder="Enter the port you want to open"
required value="{{ challenge.redirect_port }}">
</div>
<div class="form-group">
<label for="value">Docker Container Memory Limit<br>
<small class="form-text text-muted">
The memory usage limit
</small>
</label>
<input type="text" class="form-control" name="memory_limit" placeholder="Enter the memory limit"
value="{{ challenge.memory_limit }}" required>
</div>
<div class="form-group">
<label for="value">Docker Container CPU Limit<br>
<small class="form-text text-muted">
The CPU usage limit
</small>
</label>
<input type="number" class="form-control" name="cpu_limit" placeholder="Enter the cpu limit"
value="{{ challenge.cpu_limit }}" required>
</div>
<div class="form-group">
<label for="value">Score Type<br>
<small class="form-text text-muted">
Decide it use dynamic score or not
</small>
</label>
<select class="form-control" name="dynamic_score">
<option value="0" {% if challenge.dynamic_score == 0 %}selected{% endif %}>Static Score</option>
<option value="1" {% if challenge.dynamic_score == 1 %}selected{% endif %}>Dynamic Score</option>
</select>
</div>
{% endblock %}

52
assets/update.js Normal file
View File

@ -0,0 +1,52 @@
if ($ === undefined) $ = CTFd.lib.$
$('#submit-key').click(function(e) {
submitkey($('#chalid').val(), $('#answer').val())
});
$('#submit-keys').click(function(e) {
e.preventDefault();
$('#update-keys').modal('hide');
});
$('#limit_max_attempts').change(function() {
if (this.checked) {
$('#chal-attempts-group').show();
} else {
$('#chal-attempts-group').hide();
$('#chal-attempts-input').val('');
}
});
// Markdown Preview
$('#desc-edit').on('shown.bs.tab', function(event) {
if (event.target.hash == '#desc-preview') {
var editor_value = $('#desc-editor').val();
$(event.target.hash).html(
window.challenge.render(editor_value)
);
}
});
$('#new-desc-edit').on('shown.bs.tab', function(event) {
if (event.target.hash == '#new-desc-preview') {
var editor_value = $('#new-desc-editor').val();
$(event.target.hash).html(
window.challenge.render(editor_value)
);
}
});
function loadchal(id, update) {
$.get(script_root + '/admin/chal/' + id, function(obj) {
$('#desc-write-link').click(); // Switch to Write tab
if (typeof update === 'undefined')
$('#update-challenge').modal();
});
}
function openchal(id) {
loadchal(id);
}
$(document).ready(function() {
$('[data-toggle="tooltip"]').tooltip();
});

36
assets/view.html Normal file
View File

@ -0,0 +1,36 @@
{% extends "challenge.html" %}
{% block description %}
{{ challenge.html }}
<div class="row text-center pb-3">
<div id="whale-panel" style="width: 100%;">
<div id="whale-panel-stopped" class="card" style="width: 100%;">
<div class="card-body">
<h5 class="card-title">Instance Info</h5>
<button class="btn btn-primary card-link" id="whale-button-boot" type="button"
onclick="CTFd._internal.challenge.boot()">Launch an instance</button>
</div>
</div>
<div id="whale-panel-started" type="hidden" class="card" style="width: 100%;">
<div class="card-body">
<h5 class="card-title">Instance Info</h5>
<h6 class="card-subtitle mb-2 text-muted">
Remaining Time: <span id="whale-challenge-count-down"></span>s
</h6>
<h6 class="card-subtitle mb-2 text-muted">
Lan Domain: <span id="whale-challenge-lan-domain"></span>
</h6>
<p id="whale-challenge-user-access" class="card-text"></p>
<button type="button" class="btn btn-danger card-link" id="whale-button-destroy"
onclick="CTFd._internal.challenge.destroy()">
Destroy this instance
</button>
<button type="button" class="btn btn-success card-link" id="whale-button-renew"
onclick="CTFd._internal.challenge.renew()">
Renew this instance
</button>
</div>
</div>
</div>
</div>
{% endblock %}

239
assets/view.js Normal file
View File

@ -0,0 +1,239 @@
CTFd._internal.challenge.data = undefined
CTFd._internal.challenge.renderer = null;
CTFd._internal.challenge.preRender = function () {
}
CTFd._internal.challenge.render = null;
CTFd._internal.challenge.postRender = function () {
loadInfo();
}
if (window.$ === undefined) window.$ = CTFd.lib.$;
function loadInfo() {
var challenge_id = CTFd._internal.challenge.data.id;
var url = "/api/v1/plugins/ctfd-whale/container?challenge_id=" + challenge_id;
CTFd.fetch(url, {
method: 'GET',
credentials: 'same-origin',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
}
}).then(function (response) {
if (response.status === 429) {
// User was ratelimited but process response
return response.json();
}
if (response.status === 403) {
// User is not logged in or CTF is paused.
return response.json();
}
return response.json();
}).then(function (response) {
if (window.t !== undefined) {
clearInterval(window.t);
window.t = undefined;
}
if (response.success) response = response.data;
else CTFd._functions.events.eventAlert({
title: "Fail",
html: response.message,
button: "OK"
});
if (response.remaining_time != undefined) {
$('#whale-challenge-user-access').html(response.user_access);
$('#whale-challenge-lan-domain').html(response.lan_domain);
$('#whale-challenge-count-down').text(response.remaining_time);
$('#whale-panel-stopped').hide();
$('#whale-panel-started').show();
window.t = setInterval(() => {
const c = $('#whale-challenge-count-down').text();
if (!c) return;
let second = parseInt(c) - 1;
if (second <= 0) {
loadInfo();
}
$('#whale-challenge-count-down').text(second);
}, 1000);
} else {
$('#whale-panel-started').hide();
$('#whale-panel-stopped').show();
}
});
};
CTFd._internal.challenge.destroy = function () {
var challenge_id = CTFd._internal.challenge.data.id;
var url = "/api/v1/plugins/ctfd-whale/container?challenge_id=" + challenge_id;
$('#whale-button-destroy').text("Waiting...");
$('#whale-button-destroy').prop('disabled', true);
var params = {};
CTFd.fetch(url, {
method: 'DELETE',
credentials: 'same-origin',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(params)
}).then(function (response) {
if (response.status === 429) {
// User was ratelimited but process response
return response.json();
}
if (response.status === 403) {
// User is not logged in or CTF is paused.
return response.json();
}
return response.json();
}).then(function (response) {
if (response.success) {
loadInfo();
CTFd._functions.events.eventAlert({
title: "Success",
html: "Your instance has been destroyed!",
button: "OK"
});
} else {
CTFd._functions.events.eventAlert({
title: "Fail",
html: response.message,
button: "OK"
});
}
}).finally(() => {
$('#whale-button-destroy').text("Destroy this instance");
$('#whale-button-destroy').prop('disabled', false);
});
};
CTFd._internal.challenge.renew = function () {
var challenge_id = CTFd._internal.challenge.data.id;
var url = "/api/v1/plugins/ctfd-whale/container?challenge_id=" + challenge_id;
$('#whale-button-renew').text("Waiting...");
$('#whale-button-renew').prop('disabled', true);
var params = {};
CTFd.fetch(url, {
method: 'PATCH',
credentials: 'same-origin',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(params)
}).then(function (response) {
if (response.status === 429) {
// User was ratelimited but process response
return response.json();
}
if (response.status === 403) {
// User is not logged in or CTF is paused.
return response.json();
}
return response.json();
}).then(function (response) {
if (response.success) {
loadInfo();
CTFd._functions.events.eventAlert({
title: "Success",
html: "Your instance has been renewed!",
button: "OK"
});
} else {
CTFd._functions.events.eventAlert({
title: "Fail",
html: response.message,
button: "OK"
});
}
}).finally(() => {
$('#whale-button-renew').text("Renew this instance");
$('#whale-button-renew').prop('disabled', false);
});
};
CTFd._internal.challenge.boot = function () {
var challenge_id = CTFd._internal.challenge.data.id;
var url = "/api/v1/plugins/ctfd-whale/container?challenge_id=" + challenge_id;
$('#whale-button-boot').text("Waiting...");
$('#whale-button-boot').prop('disabled', true);
var params = {};
CTFd.fetch(url, {
method: 'POST',
credentials: 'same-origin',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(params)
}).then(function (response) {
if (response.status === 429) {
// User was ratelimited but process response
return response.json();
}
if (response.status === 403) {
// User is not logged in or CTF is paused.
return response.json();
}
return response.json();
}).then(function (response) {
if (response.success) {
loadInfo();
CTFd._functions.events.eventAlert({
title: "Success",
html: "Your instance has been deployed!",
button: "OK"
});
} else {
CTFd._functions.events.eventAlert({
title: "Fail",
html: response.message,
button: "OK"
});
}
}).finally(() => {
$('#whale-button-boot').text("Launch an instance");
$('#whale-button-boot').prop('disabled', false);
});
};
CTFd._internal.challenge.submit = function (preview) {
var challenge_id = CTFd._internal.challenge.data.id;
var submission = $('#challenge-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
})
};