From 958b203cdce65c2bcc97282dfbe6e60e0feb1a7d Mon Sep 17 00:00:00 2001
From: Pantelis Roditis
Date: Mon, 12 Jan 2026 10:43:32 +0200
Subject: [PATCH 01/71] fix typo in path name
---
ansible/runonce/mui.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/ansible/runonce/mui.yml b/ansible/runonce/mui.yml
index b3c247965..068b0df82 100755
--- a/ansible/runonce/mui.yml
+++ b/ansible/runonce/mui.yml
@@ -272,7 +272,7 @@
dest: "{{item.dest}}"
with_items:
- { src: '{{playbook_dir}}/../templates/httpd.conf.j2', dest: '/etc/httpd.conf', domain: '{{hostname}}' }
- - { src: '{{playbook_dir}}/../templates/acme-client.conf.j2', dest: '/etc/acme-client.conf', domain: '{{hostname}}', challenge_dir: "/home/moderatortUI/acme/.well-known/acme-challenge/" }
+ - { src: '{{playbook_dir}}/../templates/acme-client.conf.j2', dest: '/etc/acme-client.conf', domain: '{{hostname}}', challenge_dir: "/home/moderatorUI/acme/.well-known/acme-challenge/" }
- name: Generate pf tables files
command: "{{item.cmd}}"
From e22be841106af2f4a5657dfedd4de1f9abfb8adf Mon Sep 17 00:00:00 2001
From: Pantelis Roditis
Date: Mon, 12 Jan 2026 10:43:53 +0200
Subject: [PATCH 02/71] allow us to perform acme requests even when in DT mode
---
ansible/templates/dt.conf.j2 | 10 +++++++++-
1 file changed, 9 insertions(+), 1 deletion(-)
diff --git a/ansible/templates/dt.conf.j2 b/ansible/templates/dt.conf.j2
index 17f7650b4..5a3699bdc 100644
--- a/ansible/templates/dt.conf.j2
+++ b/ansible/templates/dt.conf.j2
@@ -43,7 +43,15 @@ http {
ssl_prefer_server_ciphers on;
ssl_dhparam /etc/ssl/private/dhparam.pem;
- return 503;
+ # ACME challenge must bypass maintenance
+ location ^~ /.well-known/acme-challenge/ {
+ root /acme;
+ try_files $uri =404;
+ }
+
+ location / {
+ return 503;
+ }
error_page 503 @maintenance;
location @maintenance {
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains";
From e1461fdde93fb83faae8b1362f9543482f4f5731 Mon Sep 17 00:00:00 2001
From: Pantelis Roditis
Date: Mon, 12 Jan 2026 10:44:15 +0200
Subject: [PATCH 03/71] a simple playbook to add new ssh keys to a host/user
---
ansible/maintenance/authorized_keys.yml | 33 +++++++++++++++++++++++++
1 file changed, 33 insertions(+)
create mode 100644 ansible/maintenance/authorized_keys.yml
diff --git a/ansible/maintenance/authorized_keys.yml b/ansible/maintenance/authorized_keys.yml
new file mode 100644
index 000000000..cda89202b
--- /dev/null
+++ b/ansible/maintenance/authorized_keys.yml
@@ -0,0 +1,33 @@
+---
+- name: Update authorized SSH keys for a user
+ hosts: all
+ become: true
+
+ vars:
+ target_user: root
+
+ pre_tasks:
+ - name: Ensure ssh_keys_source is provided
+ ansible.builtin.fail:
+ msg: "You must provide ssh_keys_source (file path or URL)"
+ when: ssh_keys_source is not defined
+
+ - name: Load SSH public keys from file or URL
+ ansible.builtin.set_fact:
+ ssh_public_keys: >-
+ {{
+ lookup(
+ ssh_keys_source is match('^https?://')
+ | ternary('url', 'file'),
+ ssh_keys_source
+ )
+ }}
+
+ tasks:
+ - name: Update authorized_keys for user
+ ansible.posix.authorized_key:
+ user: "{{ target_user }}"
+ key: "{{ ssh_public_keys }}"
+ manage_dir: true
+ state: present
+ # exclusive: true # uncomment to replace all existing keys
From fd4fd17021abed9ae9ad9ecf0b6829d7f8b56199 Mon Sep 17 00:00:00 2001
From: Pantelis Roditis
Date: Mon, 12 Jan 2026 10:44:54 +0200
Subject: [PATCH 04/71] sync composer lock with json
---
.github/update-composer-json.php | 137 +++++++++++++++++++++++++++++++
1 file changed, 137 insertions(+)
create mode 100644 .github/update-composer-json.php
diff --git a/.github/update-composer-json.php b/.github/update-composer-json.php
new file mode 100644
index 000000000..2144c54fb
--- /dev/null
+++ b/.github/update-composer-json.php
@@ -0,0 +1,137 @@
+ 'package', // The type is at the root level of the repository entry
+ 'package' => [
+ 'name' => $packageName,
+ 'version' => $packageVersionRange,
+ 'type' => 'library', // Default type, you can adjust this if needed
+ 'dist' => [
+ 'url' => $repositoryUrl,
+ 'type' => $distType, // Use the dynamically derived dist type
+ 'reference' => $packageHash // Adding the hash (reference) if available
+ ]
+ ]
+ ];
+ // Track pinned package
+ $pinnedPackages[] = $packageName;
+ }
+}
+
+// Write the updated composer.json back to the file
+if (file_put_contents($composerJsonPath, json_encode($composerJson, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . PHP_EOL)) {
+ echo "composer.json has been updated with the versions from composer.lock.\n";
+ if (count($pinnedPackages) > 0) {
+ echo "The following packages were pinned to repositories:\n";
+ foreach ($pinnedPackages as $packageName) {
+ echo " - $packageName\n";
+ }
+ }
+} else {
+ echo "Failed to update composer.json.\n";
+ exit(1);
+}
From 5164b74ced76d62bfe7475c350aac7f480b41747 Mon Sep 17 00:00:00 2001
From: Pantelis Roditis
Date: Wed, 14 Jan 2026 14:17:15 +0200
Subject: [PATCH 05/71] add auto-assign pr
---
.github/workflows/auto-assign-pr.yml | 21 +++++++++++++++++++++
1 file changed, 21 insertions(+)
create mode 100644 .github/workflows/auto-assign-pr.yml
diff --git a/.github/workflows/auto-assign-pr.yml b/.github/workflows/auto-assign-pr.yml
new file mode 100644
index 000000000..dcc6fb843
--- /dev/null
+++ b/.github/workflows/auto-assign-pr.yml
@@ -0,0 +1,21 @@
+name: Auto-Assign PR
+
+on:
+ pull_request:
+ types: [opened]
+
+jobs:
+ assign:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Assign PR to repo owner
+ uses: actions/github-script@v8
+ with:
+ github-token: ${{ secrets.GH_ADMIN_TOKEN }}
+ script: |
+ await github.rest.issues.addAssignees({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: context.issue.number,
+ assignees: ["proditis"]
+ });
From 0151a10a67fac092c501ba979cdbda3fd73af72d Mon Sep 17 00:00:00 2001
From: Pantelis Roditis
Date: Wed, 14 Jan 2026 14:17:31 +0200
Subject: [PATCH 06/71] create docker-servers bootstrap and normal ops
---
ansible/runonce/docker-servers.yml | 121 ++++++++++++++++-------------
1 file changed, 66 insertions(+), 55 deletions(-)
diff --git a/ansible/runonce/docker-servers.yml b/ansible/runonce/docker-servers.yml
index af2024e10..1b9d3e404 100755
--- a/ansible/runonce/docker-servers.yml
+++ b/ansible/runonce/docker-servers.yml
@@ -1,40 +1,17 @@
#!/usr/bin/env ansible-playbook
---
-- name: Configure docker servers for echoCTF
+- name: Bootstrap docker servers for echoCTF
hosts: all
gather_facts: true
become: true
become_method: su
+ remote_user: sysadmin
tasks:
+
- name: Set timezone to UTC
timezone:
name: UTC
- - name: Kill any running pm2
- shell: pm2 kill
- no_log: true
- ignore_errors: true
-
- - name: Ensure docker services are stopped
- command: service docker stop
- no_log: true
- ignore_errors: true
-
- - name: Ensure existing docker service overrides are removed
- ansible.builtin.file:
- path: "{{item}}"
- state: absent
- with_items:
- - /etc/systemd/system/docker.service.d/dockerd-service-override.conf
- - /etc/systemd/system/docker.service.d/override.conf
- - /etc/systemd/system/pm2-root.service.d/pm2-root-service-override.conf
- - /etc/systemd/system/pm2-root.service.d/override.conf
-
- - name: Remove any existing /etc/docker/daemon.json
- ansible.builtin.file:
- path: /etc/docker/daemon.json
- state: absent
-
- name: Set hostname based on host_var
hostname:
name: "{{fqdn}}"
@@ -61,29 +38,6 @@
pkg: "{{pre_apt}}"
when: pre_apt is defined and pre_apt|length > 0
- - name: Add apt keys
- when: aptKeys is defined
- apt_key:
- url: "{{item.key}}"
- state: "{{item.state}}"
- with_items: "{{aptKeys}}"
-
- - name: Add apt repositories
- when: aptRepos is defined
- apt_repository:
- repo: "{{item.repo}}"
- state: "{{item.state}}"
- with_items: "{{aptRepos}}"
-
- - name: Update package cache
- apt:
- update_cache: yes
-
- - name: Update all packages to the latest version
- no_log: "{{DEBUG|default(true)}}"
- apt:
- upgrade: dist
-
- name: Adding defined users (optional)
when: users is defined
user:
@@ -109,16 +63,73 @@
key: "https://github.com/{{item}}.keys"
with_items: "{{sshkeys}}"
- - name: Make sure sysadmin has sudo access
- lineinfile:
- path: /etc/sudoers.d/90_sysadmin
- line: 'sysadmin ALL=(ALL) NOPASSWD:ALL'
- create: yes
-
- name: Ensure /home/sysadmin is owned by sysadmin user (recursive)
shell: chown -R sysadmin /home/sysadmin
when: users is defined
+ - name: Allow user to have passwordless sudo
+ copy:
+ dest: "/etc/sudoers.d/90_sysadmin"
+ content: "sysadmin ALL=(ALL) NOPASSWD:ALL\n"
+ owner: root
+ group: root
+ mode: '0440'
+
+- name: Configure docker servers for echoCTF
+ hosts: all
+ gather_facts: true
+ become: false
+ remote_user: root
+ tasks:
+
+ - name: Kill any running pm2
+ shell: pm2 kill
+ no_log: true
+ ignore_errors: true
+
+ - name: Ensure docker services are stopped
+ command: service docker stop
+ no_log: true
+ ignore_errors: true
+
+ - name: Ensure existing docker service overrides are removed
+ ansible.builtin.file:
+ path: "{{item}}"
+ state: absent
+ with_items:
+ - /etc/systemd/system/docker.service.d/dockerd-service-override.conf
+ - /etc/systemd/system/docker.service.d/override.conf
+ - /etc/systemd/system/pm2-root.service.d/pm2-root-service-override.conf
+ - /etc/systemd/system/pm2-root.service.d/override.conf
+
+ - name: Remove any existing /etc/docker/daemon.json
+ ansible.builtin.file:
+ path: /etc/docker/daemon.json
+ state: absent
+
+ - name: Add apt keys
+ when: aptKeys is defined
+ apt_key:
+ url: "{{item.key}}"
+ state: "{{item.state}}"
+ with_items: "{{aptKeys}}"
+
+ - name: Add apt repositories
+ when: aptRepos is defined
+ apt_repository:
+ repo: "{{item.repo}}"
+ state: "{{item.state}}"
+ with_items: "{{aptRepos}}"
+
+ - name: Update package cache
+ apt:
+ update_cache: yes
+
+ - name: Update all packages to the latest version
+ no_log: "{{DEBUG|default(true)}}"
+ apt:
+ upgrade: dist
+
- name: Install post install packages
apt:
state: latest
From a2b7d7447a7b5655c32b2fe717bcc4b6425fb55d Mon Sep 17 00:00:00 2001
From: Pantelis Roditis
Date: Wed, 14 Jan 2026 14:17:51 +0200
Subject: [PATCH 07/71] dont show personal stats on team events
---
frontend/components/Img.php | 22 ++++++++++++++++------
1 file changed, 16 insertions(+), 6 deletions(-)
diff --git a/frontend/components/Img.php b/frontend/components/Img.php
index f4371ad72..75126b2e1 100644
--- a/frontend/components/Img.php
+++ b/frontend/components/Img.php
@@ -55,12 +55,22 @@ public static function profile($profile)
imagestring($image, 6, 200, $lineheight*$i++, sprintf(\Yii::t('app',"root@%s:/# ./userinfo --profile %d"),\Yii::$app->sys->offense_domain,$profile->id),$textcolor);
imagestring($image, 6, 200, $lineheight*$i++, sprintf(\Yii::t('app',"username.....: %s"),$profile->owner->username),$greencolor);
imagestring($image, 6, 200, $lineheight*$i++, sprintf(\Yii::t('app',"joined.......: %s"),date("d.m.Y", strtotime($profile->owner->created))),$greencolor);
- imagestring($image, 6, 200, $lineheight*$i++, sprintf(\Yii::t('app',"points.......: %s"),number_format($profile->owner->playerScore->points)),$greencolor);
- imagestring($image, 6, 200, $lineheight*$i++, sprintf(\Yii::t('app',"rank.........: %s"),$profile->owner->playerScore->points == 0 ? "-":$profile->rank->ordinalPlace),$greencolor);
- imagestring($image, 6, 200, $lineheight*$i++, sprintf(\Yii::t('app',"level........: %d / %s"),$profile->experience->id, $profile->experience->name),$greencolor);
- imagestring($image, 6, 200, $lineheight*$i++, sprintf(\Yii::t('app',"flags........: %d"), $profile->totalTreasures),$greencolor);
- imagestring($image, 6, 200, $lineheight*$i++, sprintf(\Yii::t('app',"challenges...: %d / %d first"),$profile->challengesSolverCount, $profile->firstChallengeSolversCount),$greencolor);
- imagestring($image, 6, 200, $lineheight*$i++, sprintf(\Yii::t('app',"headshots....: %d / %d first"),$profile->headshotsCount, $profile->firstHeadshotsCount),$greencolor);
+ if (\Yii::$app->sys->team_only_leaderboards !== true)
+ {
+ imagestring($image, 6, 200, $lineheight*$i++, sprintf(\Yii::t('app',"points.......: %s"),number_format($profile->owner->playerScore->points)),$greencolor);
+ imagestring($image, 6, 200, $lineheight*$i++, sprintf(\Yii::t('app',"rank.........: %s"),$profile->owner->playerScore->points == 0 ? "-":$profile->rank->ordinalPlace),$greencolor);
+ imagestring($image, 6, 200, $lineheight*$i++, sprintf(\Yii::t('app',"level........: %d / %s"),$profile->experience->id, $profile->experience->name),$greencolor);
+ imagestring($image, 6, 200, $lineheight*$i++, sprintf(\Yii::t('app',"flags........: %d"), $profile->totalTreasures),$greencolor);
+ imagestring($image, 6, 200, $lineheight*$i++, sprintf(\Yii::t('app',"challenges...: %d / %d first"),$profile->challengesSolverCount, $profile->firstChallengeSolversCount),$greencolor);
+ imagestring($image, 6, 200, $lineheight*$i++, sprintf(\Yii::t('app',"headshots....: %d / %d first"),$profile->headshotsCount, $profile->firstHeadshotsCount),$greencolor);
+ }
+ else if($profile->owner->teamPlayer)
+ {
+ imagestring($image, 6, 200, $lineheight*$i++, \Yii::t('app',"team.........: {team}",['team'=>$profile->owner->team->name]),$greencolor);
+ imagestring($image, 6, 200, $lineheight*$i++, \Yii::t('app',"team rank....: {rank}",['rank'=>($profile->owner->team->rank !== null ? $profile->owner->team->rank->ordinalPlace : 'empty')]),$greencolor);
+ imagestring($image, 6, 200, $lineheight*$i++, \Yii::t('app',"team points..: {points,plural,=0{0 pts} =1{# pts} other{# pts}}",['points'=>($profile->owner->team->score !== null ? $profile->owner->team->score->points : 0)]),$greencolor);
+ imagestring($image, 6, 200, $lineheight*$i++, \Yii::t('app',"contributed..: {points,plural,=0{0 pts} =1{# pts} other{# pts}}",['points'=>($profile->owner->teamStreamPoints->points ?? 0)]),$greencolor);
+ }
imagedestroy($avatar);
imagedestroy($cover);
imagedestroy($src);
From 426c95db281136f88cd2c2df9eaa8659a0ccc819 Mon Sep 17 00:00:00 2001
From: Pantelis Roditis
Date: Wed, 14 Jan 2026 14:18:30 +0200
Subject: [PATCH 08/71] dont show leaderboards on guest view profile on team
only events
---
frontend/themes/material/profile/guest/index.php | 2 ++
1 file changed, 2 insertions(+)
diff --git a/frontend/themes/material/profile/guest/index.php b/frontend/themes/material/profile/guest/index.php
index ac6ab237d..468b9cf8c 100644
--- a/frontend/themes/material/profile/guest/index.php
+++ b/frontend/themes/material/profile/guest/index.php
@@ -61,6 +61,7 @@
=$this->render('../_profile_tabs',['profile'=>$profile,'game'=>$game,'headshots'=>$headshots]);?>
+sys->team_only_leaderboards!==true):?>
+
From 201a4f7a4f2e79fe509fe90111bbd269d80ac5d4 Mon Sep 17 00:00:00 2001
From: Pantelis Roditis
Date: Wed, 14 Jan 2026 14:18:57 +0200
Subject: [PATCH 09/71] team module required for badge generation on team
events
---
frontend/config/console.php | 105 +++++++++++++++++++-----------------
1 file changed, 55 insertions(+), 50 deletions(-)
diff --git a/frontend/config/console.php b/frontend/config/console.php
index 4b146d6c7..61ad81f0e 100644
--- a/frontend/config/console.php
+++ b/frontend/config/console.php
@@ -1,54 +1,60 @@
'basic-console',
-// 'language' => 'el-GR',
- 'sourceLanguage' => 'en-US',
- 'basePath' => dirname(__DIR__),
- 'bootstrap' => ['log'],
- 'controllerNamespace' => 'app\commands',
- 'aliases' => [
- '@bower' => '@vendor/bower-asset',
- '@npm' => '@vendor/npm-asset',
- '@tests' => '@app/tests',
+$config = [
+ 'id' => 'basic-console',
+ // 'language' => 'el-GR',
+ 'sourceLanguage' => 'en-US',
+ 'basePath' => dirname(__DIR__),
+ 'bootstrap' => ['log'],
+ 'controllerNamespace' => 'app\commands',
+ 'aliases' => [
+ '@bower' => '@vendor/bower-asset',
+ '@npm' => '@vendor/npm-asset',
+ '@tests' => '@app/tests',
+ ],
+ 'modules' => [
+ 'team' => [
+ 'class' => 'app\modules\team\Module',
],
- 'components' => [
- 'i18n' => [
- 'translations' => [
- 'yii' => [
- 'class' => 'yii\i18n\PhpMessageSource',
- ],
- 'app*' => [
- 'class' => 'yii\i18n\PhpMessageSource',
- 'basePath' => '@app/messages',
- 'sourceLanguage' => 'en-US',
- 'fileMap' => [
- 'app' => 'app.php',
- 'app/error' => 'error.php',
- ],
- ],
- ],
+ ],
+
+ 'components' => [
+ 'i18n' => [
+ 'translations' => [
+ 'yii' => [
+ 'class' => 'yii\i18n\PhpMessageSource',
],
- 'sys'=> [
- 'class' => 'app\components\Sysconfig',
+ 'app*' => [
+ 'class' => 'yii\i18n\PhpMessageSource',
+ 'basePath' => '@app/messages',
+ 'sourceLanguage' => 'en-US',
+ 'fileMap' => [
+ 'app' => 'app.php',
+ 'app/error' => 'error.php',
+ ],
],
- 'cache' => $cache,
- 'log' => [
- 'targets' => [
- [
- 'class' => 'yii\log\FileTarget',
- 'levels' => ['error', 'warning'],
- ],
- ],
+ ],
+ ],
+ 'sys' => [
+ 'class' => 'app\components\Sysconfig',
+ ],
+ 'cache' => $cache,
+ 'log' => [
+ 'targets' => [
+ [
+ 'class' => 'yii\log\FileTarget',
+ 'levels' => ['error', 'warning'],
],
- 'db' => $db,
+ ],
],
- 'params' => $params,
- /*
+ 'db' => $db,
+ ],
+ 'params' => $params,
+ /*
'controllerMap' => [
'fixture' => [ // Fixture generation command line.
'class' => 'yii\faker\FixtureController',
@@ -57,13 +63,12 @@
*/
];
-if(YII_ENV_DEV)
-{
- // configuration adjustments for 'dev' environment
- $config['bootstrap'][]='gii';
- $config['modules']['gii']=[
- 'class' => 'yii\gii\Module',
- ];
+if (YII_ENV_DEV) {
+ // configuration adjustments for 'dev' environment
+ $config['bootstrap'][] = 'gii';
+ $config['modules']['gii'] = [
+ 'class' => 'yii\gii\Module',
+ ];
}
return $config;
From 2338b8b4e9aa253a26b77b78cafc41e8936e72f5 Mon Sep 17 00:00:00 2001
From: Pantelis Roditis
Date: Wed, 14 Jan 2026 19:17:48 +0200
Subject: [PATCH 10/71] add missing pass from admins
---
ansible/runonce/docker-registry.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/ansible/runonce/docker-registry.yml b/ansible/runonce/docker-registry.yml
index 547330003..49b150e46 100755
--- a/ansible/runonce/docker-registry.yml
+++ b/ansible/runonce/docker-registry.yml
@@ -137,7 +137,7 @@
content: "{{item.content|default(omit)}}"
with_items:
- { dest: "/etc/pf.conf", src: "../files/pf.conf"}
- - { dest: "/etc/service.pf.conf", content: "anchor \"dynamic\"\npass quick inet proto tcp from to port {{registry_bind_port}} label \"service_clients\"\n"}
+ - { dest: "/etc/service.pf.conf", content: "pass quick from label \"administrators\"\nanchor \"dynamic\"\npass quick inet proto tcp from to port {{registry_bind_port}} label \"service_clients\"\n"}
- { dest: "/etc/service_clients.conf", content: "{{registry_targets_cidr}}\n"}
- name: Dump administrators PF table (if exists)
From ce1ed68ec5ad667f63b016066b2e3dc2a183c127 Mon Sep 17 00:00:00 2001
From: Pantelis Roditis
Date: Wed, 14 Jan 2026 19:20:02 +0200
Subject: [PATCH 11/71] fix the typo in targets_cidr
---
ansible/inventories/servers/host_vars/registry.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/ansible/inventories/servers/host_vars/registry.yml b/ansible/inventories/servers/host_vars/registry.yml
index 87ad4d3cc..37fa85f1e 100644
--- a/ansible/inventories/servers/host_vars/registry.yml
+++ b/ansible/inventories/servers/host_vars/registry.yml
@@ -5,7 +5,7 @@ registry_storage: "/storage"
registry_bind_ip: "0.0.0.0"
registry_bind_port: "5000"
registry_targets_if: if0
-registry_targets_cidr: 10.0.100.0/24
+registry_targets_cidr: 10.0.0.100/24
backups:
- { tgz: "/altroot/root.tgz", src: '/root' }
- { tgz: "/altroot/etc.tgz", src: '/etc' }
From 5b0400a032b25c8b110ecd2bc212054a94d4b612 Mon Sep 17 00:00:00 2001
From: Pantelis Roditis
Date: Thu, 15 Jan 2026 10:30:16 +0200
Subject: [PATCH 12/71] disable dynamic treasures for sanitycheck
---
ansible/Dockerfiles/sanitycheck/variables.yml | 1 +
1 file changed, 1 insertion(+)
diff --git a/ansible/Dockerfiles/sanitycheck/variables.yml b/ansible/Dockerfiles/sanitycheck/variables.yml
index 9766edd51..1762de0cb 100644
--- a/ansible/Dockerfiles/sanitycheck/variables.yml
+++ b/ansible/Dockerfiles/sanitycheck/variables.yml
@@ -12,6 +12,7 @@ writeup_allowed: 0
headshot_spin: 0
instance_allowed: 0
TargetOndemand: false
+dynamic_treasures: 0
container:
name: "{{hostname}}"
hostname: "{{fqdn}}"
From e5f69418cb94de0b059ee9161658001af7eb21d1 Mon Sep 17 00:00:00 2001
From: Pantelis Roditis
Date: Sun, 18 Jan 2026 13:55:59 +0200
Subject: [PATCH 13/71] support expire, typecast params, catch Throwable
instead of exceptions
---
backend/commands/CronController.php | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/backend/commands/CronController.php b/backend/commands/CronController.php
index ec16a5bc0..9a4b0e5dd 100644
--- a/backend/commands/CronController.php
+++ b/backend/commands/CronController.php
@@ -225,7 +225,7 @@ public function actionInstancePfTables($dopf = false)
/**
* Process player private instances
*/
- public function actionInstances($pfonly = false)
+ public function actionInstances(bool $pfonly = false,int $expire = 40)
{
if (file_exists("/tmp/cron-instances.lock")) {
echo date("Y-m-d H:i:s ") . "Instances: /tmp/cron-instances.lock exists, skipping execution\n";
@@ -242,7 +242,7 @@ public function actionInstances($pfonly = false)
}
}
- $t = TargetInstance::find()->pending_action(40);
+ $t = TargetInstance::find()->pending_action($expire);
foreach ($t->all() as $val) {
try {
$ips = [];
@@ -298,7 +298,7 @@ public function actionInstances($pfonly = false)
if ($pfonly === false) {
try {
$dc->destroy();
- } catch (\Exception $e) {
+ } catch (\Throwable $e) {
}
$dc->pull();
$dc->spin();
@@ -337,7 +337,7 @@ public function actionInstances($pfonly = false)
default:
printf("Error: Unknown action\n");
}
- } catch (\Exception $e) {
+ } catch (\Throwable $e) {
if (method_exists($e, 'getErrorResponse'))
echo "Instances:", $e->getErrorResponse()->getMessage(), "\n";
else
From 71e42f21510a0b3df2950f3b75dd144fc91465c9 Mon Sep 17 00:00:00 2001
From: Pantelis Roditis
Date: Sun, 18 Jan 2026 13:56:49 +0200
Subject: [PATCH 14/71] avoid filtering on 0 minutes_ago
---
.../modules/infrastructure/models/TargetInstanceQuery.php | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/backend/modules/infrastructure/models/TargetInstanceQuery.php b/backend/modules/infrastructure/models/TargetInstanceQuery.php
index b2551b250..7af994882 100644
--- a/backend/modules/infrastructure/models/TargetInstanceQuery.php
+++ b/backend/modules/infrastructure/models/TargetInstanceQuery.php
@@ -14,9 +14,11 @@ public function active()
return $this->andWhere('[[ip]] IS NOT NULL')->andWhere('[[reboot]]!=2');
}
- public function pending_action($minutes_ago = 60)
+ public function pending_action(int $minutes_ago = 60)
{
- return $this->andWhere('[[ip]] IS NULL')->orWhere(['>', 'reboot', 0])->orWhere(['<', 'updated_at', new \yii\db\Expression("NOW() - INTERVAL $minutes_ago MINUTE")]);
+ if($minutes_ago !== 0)
+ return $this->andWhere('[[ip]] IS NULL')->orWhere(['>', 'reboot', 0])->orWhere(['<', 'updated_at', new \yii\db\Expression("NOW() - INTERVAL $minutes_ago MINUTE")]);
+ return $this->andWhere('[[ip]] IS NULL')->orWhere(['>', 'reboot', 0]);
}
From 264df81d1b8d24408bbc56b856233c04798a10bc Mon Sep 17 00:00:00 2001
From: Pantelis Roditis
Date: Sun, 18 Jan 2026 13:57:10 +0200
Subject: [PATCH 15/71] only allow mui to fetch identificationFiles
---
ansible/templates/nginx.conf.j2 | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/ansible/templates/nginx.conf.j2 b/ansible/templates/nginx.conf.j2
index f2584dcc6..fc537052e 100644
--- a/ansible/templates/nginx.conf.j2
+++ b/ansible/templates/nginx.conf.j2
@@ -252,6 +252,15 @@ http {
set $yii_bootstrap "/index.php";
{% if "mui" not in inventory_hostname %}
+ {% if mui_ext_ip is defined %}
+ location /identificationFiles/ {
+ allow {{ mui_ext_ip }};
+ deny all;
+
+ try_files $uri $uri/ =404;
+ }
+ {% endif %}
+
# Rate limit /api to 10 requests/sec
# This is way too loose
location /api {
From 904841d3b4ae53a7b3c38bb4c81fddd8ee32f0c0 Mon Sep 17 00:00:00 2001
From: Pantelis Roditis
Date: Fri, 23 Jan 2026 19:15:07 +0200
Subject: [PATCH 16/71] fix permissions and allow run from outside PR
---
.github/workflows/auto-assign-pr.yml | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/.github/workflows/auto-assign-pr.yml b/.github/workflows/auto-assign-pr.yml
index dcc6fb843..fecb812bc 100644
--- a/.github/workflows/auto-assign-pr.yml
+++ b/.github/workflows/auto-assign-pr.yml
@@ -1,9 +1,12 @@
name: Auto-Assign PR
on:
- pull_request:
+ pull_request_target:
types: [opened]
+permissions:
+ issues: write
+
jobs:
assign:
runs-on: ubuntu-latest
From 10dc3a16d5e7c299240e2de5ac962bc59edc00aa Mon Sep 17 00:00:00 2001
From: Pantelis Roditis
Date: Fri, 23 Jan 2026 19:15:31 +0200
Subject: [PATCH 17/71] no need for everyone to have access to the WS
---
ansible/files/pui.service.conf | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/ansible/files/pui.service.conf b/ansible/files/pui.service.conf
index fb1fa8ea0..9dd62ce24 100644
--- a/ansible/files/pui.service.conf
+++ b/ansible/files/pui.service.conf
@@ -1,6 +1,8 @@
# Allow users to connect to port 80/443
pass in quick on egress inet proto tcp from {, } to (egress:0) port 8888 rdr-to 127.0.0.1
+pass in quick on interconnect inet proto tcp from (interconnect:network) to (interconnect:0) port 8888 rdr-to 127.0.0.1
+
pass quick from label "administrators"
pass quick inet proto tcp from to (egress:0) port { 80 , 443 } label "www-moderators"
@@ -8,9 +10,7 @@ pass quick inet proto tcp from to (egress:0) port { 80 , 443 } labe
# FOR DT OPERATIONS
pass in quick inet proto tcp from to port 80 rdr-to 127.0.0.1 port 8080 label "maintenance"
pass in quick inet proto tcp from to port 443 rdr-to 127.0.0.1 port 8443 label "maintenance"
-block in quick on egress inet proto tcp from to (egress:0) port 8888
pass in on egress inet proto tcp from to port { 80, 443 } label "www-normal"
pass in on egress inet proto tcp to port { 80, 443 } label "www-normal"
-pass in quick on egress inet proto tcp to (egress:0) port 8888 rdr-to 127.0.0.1
From 924e911f2d7725146eebe29f69ce96d9b35c0e4d Mon Sep 17 00:00:00 2001
From: Pantelis Roditis
Date: Fri, 23 Jan 2026 19:16:15 +0200
Subject: [PATCH 18/71] sync with github repo playbook
---
ansible/maintenance/sync-github.yml | 32 +++++++++++++++++++++++++++++
1 file changed, 32 insertions(+)
create mode 100644 ansible/maintenance/sync-github.yml
diff --git a/ansible/maintenance/sync-github.yml b/ansible/maintenance/sync-github.yml
new file mode 100644
index 000000000..e413658f2
--- /dev/null
+++ b/ansible/maintenance/sync-github.yml
@@ -0,0 +1,32 @@
+#!/usr/bin/env ansible-playbook
+---
+- name: Update deployed application from Git
+ hosts: all
+ tasks:
+ - name: Ensure repository is up to date
+ git:
+ repo: "{{ GITHUB_REPO }}"
+ dest: "{{ REPO }}"
+ version: "{{ GITHUB_REPO_BRANCH }}"
+ force: yes
+ accept_hostkey: yes
+ update: yes
+ notify: restart applications
+ register: git_result
+
+ - name: Clean untracked files (preserving paths)
+ shell: git clean -fd {{ '-n' if ansible_check_mode else '' }} {% for p in PRESERVE_PATHS %}-e {{ p }} {% endfor %}
+ args:
+ chdir: "{{ REPO }}"
+ register: clean_result
+ changed_when: clean_result.stdout != ""
+ when: git_result.changed and PRESERVE_PATHS is defined
+ notify: restart applications
+
+ handlers:
+ - name: restart applications
+ when: APP_SERVICES is defined
+ service:
+ name: "{{ item }}"
+ state: restarted
+ loop: "{{ APP_SERVICES }}"
From 42167589a4ebbc4fe6d646209b8668e6e4c95a79 Mon Sep 17 00:00:00 2001
From: Pantelis Roditis
Date: Fri, 23 Jan 2026 19:17:08 +0200
Subject: [PATCH 19/71] use install -d instead to limit the number of command
from mkdir/chown
---
ansible/runonce/mui.yml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/ansible/runonce/mui.yml b/ansible/runonce/mui.yml
index 068b0df82..4f27b56e5 100755
--- a/ansible/runonce/mui.yml
+++ b/ansible/runonce/mui.yml
@@ -425,8 +425,8 @@
- name: configure folders and perms
command: "{{item}}"
with_items:
- - "mkdir -p /home/moderatorUI/{{domain_name}}/backend/web/assets"
- - "chown -R moderatorUI /home/moderatorUI/{{domain_name}}/backend/web/assets"
+ - "install -d -o moderatorUI /home/moderatorUI/{{domain_name}}/backend/web/assets"
+ - "install -d -o moderatorUI /home/moderatorUI/{{domain_name}}/backend/web/identifcationFiles"
- "mkdir -p /var/log/cron"
- "ln -sf /home/moderatorUI/{{domain_name}}/backend/yii /usr/local/bin/backend"
From 22c44083f9c4fce78e3c1619efdbd14975a6c8a0 Mon Sep 17 00:00:00 2001
From: Pantelis Roditis
Date: Fri, 23 Jan 2026 19:18:04 +0200
Subject: [PATCH 20/71] use the proper ws socket for live installs and use the
same user for dt.conf
---
ansible/runonce/pui.yml | 22 +++++++++++-----------
1 file changed, 11 insertions(+), 11 deletions(-)
diff --git a/ansible/runonce/pui.yml b/ansible/runonce/pui.yml
index 9a1ed2bf4..c23f90dd2 100755
--- a/ansible/runonce/pui.yml
+++ b/ansible/runonce/pui.yml
@@ -417,7 +417,7 @@
src: ../templates/dt.conf.j2
dest: /etc/nginx/dt.conf
with_items:
- - { user: 'www', domain: '{{domain_name}}', root: "/htdocs" }
+ - { user: 'participantUI', domain: '{{domain_name}}', root: "/htdocs" }
tags:
- nginx
@@ -466,6 +466,7 @@
- { section: PHP, option: "error_reporting", value: "E_NONE" }
- { section: PHP, option: "post_max_size", value: "180M" }
- { section: PHP, option: "upload_max_filesize", value: "120M" }
+ - { section: PHP, option: "allow_url_fopen", value: "On"}
- { section: Session, option: "session.save_handler", value: "memcached"}
- { section: Session, option: "session.save_path", value: "{{db_ip}}:11211"}
- { section: Session, option: "session.gc_maxlifetime", value: "43200" }
@@ -487,13 +488,10 @@
- name: configure folders and permissions
command: "{{item}}"
with_items:
- - mkdir -p /home/participantUI/{{domain_name}}/frontend/web/assets
- - mkdir -p /home/participantUI/{{domain_name}}/frontend/web/identificationFiles
- - mkdir -p /home/participantUI/{{domain_name}}/frontend/web/images/avatars/team
+ - install -d -o participantUI /home/participantUI/{{domain_name}}/frontend/web/assets
+ - install -d -o participantUI /home/participantUI/{{domain_name}}/frontend/web/identificationFiles
+ - install -d -o participantUI /home/participantUI/{{domain_name}}/frontend/web/images/avatars/team
- mkdir -p /var/log/cron
- - chown -R participantUI /home/participantUI/{{domain_name}}/frontend/web/assets
- - chown -R participantUI /home/participantUI/{{domain_name}}/frontend/web/images/avatars/
- - chown -R participantUI /home/participantUI/{{domain_name}}/frontend/web/identificationFiles
- ln -sf /home/participantUI/{{domain_name}}/frontend/yii /usr/local/bin/frontend
- name: configure participant rc.d
@@ -635,10 +633,12 @@
redirect_stderr=true
- name: "Update frontend/config/params.php"
- replace:
- path: "/home/participantUI/{{domain_name}}/frontend/config/params.php"
- regexp: 'ws://localhost:8888/ws'
- replace: 'wss://{{domain_name}}/ws'
+ ansible.builtin.lineinfile:
+ path: "/home/participantUI/{{ domain_name }}/frontend/config/params.php"
+ search_string: 'ws://localhost:8888/ws'
+ line: 'wss://{{ domain_name }}/ws'
+ insertafter: null
+ insertbefore: null
- name: copy nstables script
copy:
From fc8f59dc3b4958a75d9085dec220cec73861f737 Mon Sep 17 00:00:00 2001
From: Pantelis Roditis
Date: Fri, 23 Jan 2026 19:18:33 +0200
Subject: [PATCH 21/71] add php ini that is needed for our service publisher
---
ansible/runonce/vpngw.yml | 13 +++++++++++++
1 file changed, 13 insertions(+)
diff --git a/ansible/runonce/vpngw.yml b/ansible/runonce/vpngw.yml
index 495bcef33..c59c46cbe 100755
--- a/ansible/runonce/vpngw.yml
+++ b/ansible/runonce/vpngw.yml
@@ -252,6 +252,19 @@
- name: Activate install php modules
shell: "cp /etc/php-{{versions.PHP}}.sample/*.ini /etc/php-{{versions.PHP}}/"
+ - name: Fixing /etc/php.ini
+ ini_file: dest=/etc/php-{{versions.PHP}}.ini section={{item.section}} option={{item.option}} value={{item.value}} mode=0644 owner=root group=wheel
+ with_items:
+ - { section: PHP, option: "expose_php", value: "Off" }
+ - { section: PHP, option: "log_errors_max_len", value: 4096 }
+ - { section: PHP, option: "html_errors", value: "Off" }
+ - { section: PHP, option: "max_execution_time", value: "60" }
+ - { section: PHP, option: "max_input_time", value: "120" }
+ - { section: PHP, option: "memory_limit", value: "256M" }
+ - { section: PHP, option: "error_reporting", value: "E_NONE" }
+ - { section: PHP, option: "post_max_size", value: "180M" }
+ - { section: PHP, option: "upload_max_filesize", value: "120M" }
+ - { section: PHP, option: "allow_url_fopen", value: "On"}
- name: Update my.cnf
ini_file:
From 7de4ed6296ee0327a55200c46b3c98865b7901c3 Mon Sep 17 00:00:00 2001
From: Pantelis Roditis
Date: Fri, 23 Jan 2026 19:18:52 +0200
Subject: [PATCH 22/71] make sure we only allow mui to acceess
identificationFiles
---
ansible/templates/nginx.conf.j2 | 26 +++++++++++++-------------
1 file changed, 13 insertions(+), 13 deletions(-)
diff --git a/ansible/templates/nginx.conf.j2 b/ansible/templates/nginx.conf.j2
index fc537052e..30552f110 100644
--- a/ansible/templates/nginx.conf.j2
+++ b/ansible/templates/nginx.conf.j2
@@ -253,7 +253,7 @@ http {
set $yii_bootstrap "/index.php";
{% if "mui" not in inventory_hostname %}
{% if mui_ext_ip is defined %}
- location /identificationFiles/ {
+ location ^~ /identificationFiles/ {
allow {{ mui_ext_ip }};
deny all;
@@ -345,18 +345,18 @@ http {
return 404;
}
{% endif %}
- location /ws {
- proxy_pass http://127.0.0.1:8888/ws;
-
- # WebSocket-specific headers
- proxy_http_version 1.1;
- proxy_set_header Upgrade $http_upgrade;
- proxy_set_header Connection "Upgrade";
-
- # Optional: increase timeout for long-lived connections
- proxy_read_timeout 86400s;
- proxy_send_timeout 86400s;
- }
+ location /ws {
+ proxy_pass http://127.0.0.1:8888/ws;
+
+ # WebSocket-specific headers
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "Upgrade";
+
+ # Optional: increase timeout for long-lived connections
+ proxy_read_timeout 86400s;
+ proxy_send_timeout 86400s;
+ }
{% if captcha is defined %}
# Check for requests to /profile/
location ~ ^/profile/\d+$ {
From 58a3e773373e3f7b5b0b2c2a1971880242742052 Mon Sep 17 00:00:00 2001
From: Pantelis Roditis
Date: Fri, 23 Jan 2026 19:19:47 +0200
Subject: [PATCH 23/71] simplify actionInstances() and document action
---
backend/commands/CronController.php | 82 ++++++++++++++++-------------
1 file changed, 44 insertions(+), 38 deletions(-)
diff --git a/backend/commands/CronController.php b/backend/commands/CronController.php
index 9a4b0e5dd..9c7a1fa31 100644
--- a/backend/commands/CronController.php
+++ b/backend/commands/CronController.php
@@ -15,6 +15,7 @@
use app\modules\infrastructure\models\TargetInstanceAudit;
use app\modules\infrastructure\models\NetworkTargetSchedule as NTS;
use app\modules\gameplay\models\NetworkTarget;
+use app\components\helpers\ArrayHelperExtended;
/**
* @method docker_connect()
@@ -159,11 +160,7 @@ public function actionInstancePf($before = 60)
case SELF::ACTION_START:
case SELF::ACTION_RESTART:
if (($val->team_allowed === true || \Yii::$app->sys->team_visible_instances === true) && $val->player->teamPlayer !== null && $val->player->teamPlayer->approved === 1) {
- foreach ($val->player->teamPlayer->team->approvedMembers as $teamPlayer) {
- if ((int)$teamPlayer->player->last->vpn_local_address !== 0) {
- $ips[] = long2ip($teamPlayer->player->last->vpn_local_address);
- }
- }
+ $ips = $val->player->teamPlayer->team->approvedMemberIPs;
} else if ((int)$val->player->last->vpn_local_address !== 0) {
$ips[] = long2ip($val->player->last->vpn_local_address);
}
@@ -200,15 +197,9 @@ public function actionInstancePfTables($dopf = false)
}
$team_visible_instances = \Yii::$app->sys->team_visible_instances;
if ($val->team_allowed == true || $team_visible_instances === true) {
- // get team members
if ($val->player->teamPlayer) {
$team = $val->player->teamPlayer->team;
- foreach ($team->approvedMembers as $member) {
- // get IP's of connected players
- if (($pIP = $member->player->last->vpn_local_address) !== null) {
- $IPs[] = long2ip($pIP);
- }
- }
+ $IPs = ArrayHelperExtended::mergeUnique($IPs, $team->approvedMemberIPs);
}
}
@@ -224,8 +215,16 @@ public function actionInstancePfTables($dopf = false)
/**
* Process player private instances
+ *
+ * This command updates the last updated_at field for active instances
+ * and processes instances that are pending action (eg. reboot, destroy, powerup)
+ * and updates the PF according to currently online players.
+ *
+ * @param bool $pfonly Perform PF operations and skip instance docker actions (default: false)
+ * @param int $expired_ago Filter instances based that haven't been updated X seconds ago (default: 2400 seconds = 40 minutes)
+ * @return int Exit code
*/
- public function actionInstances(bool $pfonly = false,int $expire = 40)
+ public function actionInstances(bool $pfonly = false, int $expired_ago = 2400)
{
if (file_exists("/tmp/cron-instances.lock")) {
echo date("Y-m-d H:i:s ") . "Instances: /tmp/cron-instances.lock exists, skipping execution\n";
@@ -233,21 +232,13 @@ public function actionInstances(bool $pfonly = false,int $expire = 40)
}
touch("/tmp/cron-instances.lock");
$action = SELF::ACTION_EXPIRED;
- // Get powered instances
- $t = TargetInstance::find()->active();
- foreach ($t->all() as $instance) {
- if ($instance->player->last->vpn_local_address !== null && $pfonly === false) {
- printf("Updating heartbeat [%d: %s for %d: %s]\n", $instance->target_id, $instance->target->name, $instance->player_id, $instance->player->username);
- $instance->touch('updated_at');
- }
- }
-
- $t = TargetInstance::find()->pending_action($expire);
+ $t = TargetInstance::find()->pending_action($expired_ago);
foreach ($t->all() as $val) {
try {
$ips = [];
$dc = new DockerContainer($val->target);
- $dc->timeout = ($val->server->timeout ? $val->server->timeout : 2000);
+ $dc->timeout = ($val->server->timeout ? $val->server->timeout : 10000);
+
if ($val->target->targetVolumes !== null)
$dc->targetVolumes = $val->target->targetVolumes;
@@ -279,17 +270,18 @@ public function actionInstances(bool $pfonly = false,int $expire = 40)
$dc->server = $val->server->connstr;
$dc->net = $val->server->network;
- if ($val->ip == null) {
- echo date("Y-m-d H:i:s ") . "Starting";
- $action = SELF::ACTION_START;
+ if ($val->reboot === 2) {
+ echo date("Y-m-d H:i:s ") . "Destroying";
+ $action = SELF::ACTION_DESTROY;
} else if ($val->reboot === 1) {
echo date("Y-m-d H:i:s ") . "Restarting";
$action = SELF::ACTION_RESTART;
- } else if ($val->reboot === 2) {
- echo date("Y-m-d H:i:s ") . "Destroying";
- $action = SELF::ACTION_DESTROY;
+ } else if ($val->ip == null && $val->reboot == 0) {
+ echo date("Y-m-d H:i:s ") . "Starting";
+ $action = SELF::ACTION_START;
} else {
echo date("Y-m-d H:i:s ") . "Expiring";
+ $action = SELF::ACTION_EXPIRED;
}
printf(" %s for %s (%s) at %s\n", $val->target->name, $val->player->username, $dc->name, $val->server->name);
switch ($action) {
@@ -304,11 +296,7 @@ public function actionInstances(bool $pfonly = false,int $expire = 40)
$dc->spin();
}
if (($val->team_allowed === true || \Yii::$app->sys->team_visible_instances === true) && $val->player->teamPlayer && $val->player->teamPlayer->approved === 1) {
- foreach ($val->player->teamPlayer->team->approvedMembers as $teamPlayer) {
- if ((int)$teamPlayer->player->last->vpn_local_address !== 0) {
- $ips[] = long2ip($teamPlayer->player->last->vpn_local_address);
- }
- }
+ $ips = $val->player->teamPlayer->team->approvedMemberIPs;
} else if ((int)$val->player->last->vpn_local_address !== 0) {
$ips[] = long2ip($val->player->last->vpn_local_address);
}
@@ -317,8 +305,10 @@ public function actionInstances(bool $pfonly = false,int $expire = 40)
$val->ipoctet = $dc->container->getNetworkSettings()->getNetworks()->{$val->server->network}->getIPAddress();
Pf::add_table_ip($dc->name, $val->ipoctet, true);
$val->reboot = 0;
- if ($pfonly === false)
+ if ($pfonly === false) {
+ $val->updated_at = new \yii\db\Expression('NOW()');
$val->save();
+ }
break;
case SELF::ACTION_EXPIRED:
@@ -339,12 +329,28 @@ public function actionInstances(bool $pfonly = false,int $expire = 40)
}
} catch (\Throwable $e) {
if (method_exists($e, 'getErrorResponse'))
- echo "Instances:", $e->getErrorResponse()->getMessage(), "\n";
+ echo "Instances: ", $e->getErrorResponse()->getMessage(), "\n";
else
- echo "Instances:", $e->getMessage(), "\n";
+ echo "Instances: ", $e->getMessage(), "\n";
+ if (getenv('DEBUG', true) !== false)
+ \Yii::error($e);
}
+ unset($dc);
+ usleep(100);
} // end foreach
$this->actionInstancePfTables(true);
+
+ if ($pfonly === false) {
+ try {
+ $t = TargetInstance::find()->active()->withApprovedMemberHeartbeat()->last_updated(round($expired_ago / 2));
+ foreach ($t->all() as $instance) {
+ printf("Updating heartbeat [%d: %s for %d: %s]\n", $instance->target_id, $instance->target->name, $instance->player_id, $instance->player->username);
+ $instance->touch('updated_at');
+ }
+ } catch (\Throwable $e) {
+ echo "Instrances: Error while touching updated_at. ", $e->getMessage();
+ }
+ }
@unlink("/tmp/cron-instances.lock");
}
From 9040674fb0d4054bfae3e7aa6eddbdf5750421cd Mon Sep 17 00:00:00 2001
From: Pantelis Roditis
Date: Fri, 23 Jan 2026 19:20:14 +0200
Subject: [PATCH 24/71] make sure player register respect status and active
---
backend/commands/PlayerController.php | 9 ++++-----
1 file changed, 4 insertions(+), 5 deletions(-)
diff --git a/backend/commands/PlayerController.php b/backend/commands/PlayerController.php
index 586d94869..ca6040328 100644
--- a/backend/commands/PlayerController.php
+++ b/backend/commands/PlayerController.php
@@ -182,7 +182,7 @@ public function actionMail($active = false, $email = false, $status = 9, $approv
/*
Register Users and generate OpenVPN keys and settings
*/
- public function actionRegister($username, $email, $fullname, $password = false, $player_type = "offense", $active = false, $academic = false, $team_name = false, $approved = 0)
+ public function actionRegister($username, $email, $fullname, $password = false, $player_type = "offense", bool $active = true, int $status=9, $academic = false, $team_name = false, $approved = 0)
{
echo "Registering: ", $email, "\n";
$trans = Yii::$app->db->beginTransaction();
@@ -210,16 +210,15 @@ public function actionRegister($username, $email, $fullname, $password = false,
$player->password = Yii::$app->security->generatePasswordHash($password);
$player->active = intval($active);
- $player->status = 10;
+ $player->status = $status;
$player->auth_key = Yii::$app->security->generateRandomString();
if (!$player->saveWithSsl()) {
- if (!$player->active && intval(\Yii::$app->sys->disable_mailer)==0) {
+ if (!$player->active && $players->status===9 && intval(\Yii::$app->sys->disable_mailer)==0) {
$player->generateEmailVerificationToken();
- $player->status = 9;
}
- throw new ConsoleException('Failed to save player:' . $player->username, "\n");
+ throw new ConsoleException('Failed to save player:' . $player->username. "\n");
}
$player->createTeam($team_name, $approved);
From b8a953212fa4dee3a60b7bdcad39374059d90b25 Mon Sep 17 00:00:00 2001
From: Pantelis Roditis
Date: Fri, 23 Jan 2026 19:20:48 +0200
Subject: [PATCH 25/71] Introduce ArrayHelperExtended
---
.../helpers/ArrayHelperExtended.php | 27 +++++++++++++++++++
1 file changed, 27 insertions(+)
create mode 100644 backend/components/helpers/ArrayHelperExtended.php
diff --git a/backend/components/helpers/ArrayHelperExtended.php b/backend/components/helpers/ArrayHelperExtended.php
new file mode 100644
index 000000000..b170f8b48
--- /dev/null
+++ b/backend/components/helpers/ArrayHelperExtended.php
@@ -0,0 +1,27 @@
+
Date: Fri, 23 Jan 2026 19:21:40 +0200
Subject: [PATCH 26/71] Add TeamInvite and log errorSummary on errors
---
backend/modules/frontend/models/Player.php | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/backend/modules/frontend/models/Player.php b/backend/modules/frontend/models/Player.php
index 97aec5de9..346eb72d3 100644
--- a/backend/modules/frontend/models/Player.php
+++ b/backend/modules/frontend/models/Player.php
@@ -54,6 +54,7 @@ public function getAuthKey()
public function saveWithSsl()
{
if (!$this->save()) {
+ Yii::error($this->getErrorSummary(true));
return false;
}
@@ -63,6 +64,7 @@ public function saveWithSsl()
if ($playerSsl->save()) {
return $playerSsl->refresh();
}
+ Yii::error($playerSsl->getErrorSummary(true));
return false;
}
@@ -144,6 +146,9 @@ public function createTeam($team_name, $approved)
if (!$tp->save())
echo Yii::t('app', "Error saving team player\n");
+ $ti = new \app\modules\frontend\models\TeamInvite(['team_id' => $tp->id, 'token' => Yii::$app->security->generateRandomString(8)]);
+ if (!$ti->save())
+ echo Yii::t('app', "Error saving team invite\n");
}
/**
From 978d0f7ed849e3b56a5469bb4338718eb07d1595 Mon Sep 17 00:00:00 2001
From: Pantelis Roditis
Date: Fri, 23 Jan 2026 19:22:04 +0200
Subject: [PATCH 27/71] re-generate serial until unique
---
backend/modules/frontend/models/PlayerSsl.php | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/backend/modules/frontend/models/PlayerSsl.php b/backend/modules/frontend/models/PlayerSsl.php
index 6e8bdae84..0cec4d946 100644
--- a/backend/modules/frontend/models/PlayerSsl.php
+++ b/backend/modules/frontend/models/PlayerSsl.php
@@ -107,7 +107,9 @@ public function generate() {
$CAprivkey=array("file://".$tmpCAprivkey, null);
file_put_contents($tmpCAprivkey, Yii::$app->sys->{'CA.key'});
file_put_contents($tmpCAcert, Yii::$app->sys->{'CA.crt'});
- $serial=time();
+ do {
+ $serial=time();
+ } while (self::findOne(['serial'=>$serial])!=null);
$x509=openssl_csr_sign($csr, $CAcert, $CAprivkey, 3650, array('digest_alg'=>'sha256', 'config'=>Yii::getAlias('@appconfig').'/CA.cnf', 'x509_extensions'=>'usr_cert'), $serial);
openssl_csr_export($csr, $csrout);
openssl_x509_export($x509, $certout, false);
From aea78c3fc32b2bc607fb89f8bd4dafb4c10b9529 Mon Sep 17 00:00:00 2001
From: Pantelis Roditis
Date: Fri, 23 Jan 2026 19:23:07 +0200
Subject: [PATCH 28/71] add approvedMemberIP's method to team
---
backend/modules/frontend/models/Team.php | 28 +++++++++++++++++++-----
1 file changed, 23 insertions(+), 5 deletions(-)
diff --git a/backend/modules/frontend/models/Team.php b/backend/modules/frontend/models/Team.php
index ce40c889d..10cf9bb84 100644
--- a/backend/modules/frontend/models/Team.php
+++ b/backend/modules/frontend/models/Team.php
@@ -22,6 +22,7 @@
* @property Player $owner
* @property TeamPlayer[] $teamPlayers
* @property Player[] $players
+ * @property string[] $approvedMemberIPs
*/
class Team extends \yii\db\ActiveRecord
{
@@ -98,11 +99,12 @@ public function getTeamPlayers()
public function getInstances()
{
- return $this->hasMany(\app\modules\infrastructure\models\TargetInstance::class, ['player_id' => 'player_id'])
- ->via('teamPlayers', function($query) {
- $query->andWhere(['approved' => 1]);
- });
+ return $this->hasMany(\app\modules\infrastructure\models\TargetInstance::class, ['player_id' => 'player_id'])
+ ->via('teamPlayers', function ($query) {
+ $query->andWhere(['approved' => 1]);
+ });
}
+
/**
* @return \yii\db\ActiveQuery
*/
@@ -111,6 +113,22 @@ public function getApprovedMembers()
return $this->hasMany(TeamPlayer::class, ['team_id' => 'id'])->andWhere(['approved' => 1]);
}
+ /**
+ * Get array of all approved members currently online
+ *
+ * @return array
+ */
+ public function getApprovedMemberIPs()
+ {
+ $ips=[];
+ foreach ($this->approvedMembers as $tp) {
+ if ((int)$tp->player->last->vpn_local_address !== 0) {
+ $ips[] = long2ip($tp->player->last->vpn_local_address);
+ }
+ }
+ return $ips;
+ }
+
/**
* @return \yii\db\ActiveQuery
*/
@@ -197,6 +215,6 @@ public function getInvite()
*/
public function repopulateStream()
{
- return \Yii::$app->db->createCommand('CALL repopulate_team_stream(:tid)',[':tid'=>$this->id])->execute();
+ return \Yii::$app->db->createCommand('CALL repopulate_team_stream(:tid)', [':tid' => $this->id])->execute();
}
}
From f30dd643aeec0dee7ce01f9ecf7f90dc37f82c12 Mon Sep 17 00:00:00 2001
From: Pantelis Roditis
Date: Fri, 23 Jan 2026 19:23:33 +0200
Subject: [PATCH 29/71] fix targetInstanceQuery withApprovedMemberHeartbeat
---
.../models/TargetInstanceQuery.php | 44 ++++++++++++++++---
1 file changed, 39 insertions(+), 5 deletions(-)
diff --git a/backend/modules/infrastructure/models/TargetInstanceQuery.php b/backend/modules/infrastructure/models/TargetInstanceQuery.php
index 7af994882..da1067901 100644
--- a/backend/modules/infrastructure/models/TargetInstanceQuery.php
+++ b/backend/modules/infrastructure/models/TargetInstanceQuery.php
@@ -9,16 +9,50 @@
*/
class TargetInstanceQuery extends \yii\db\ActiveQuery
{
+ /**
+ * Filters TargetInstances to those that are active
+ * and whose team has at least one approved member with vpn_local_address != 0
+ */
+ public function withApprovedMemberHeartbeat()
+ {
+ return $this
+ // join to the team_instance player with teamPlayer
+ ->innerJoin(['tp' => 'team_player'], 'target_instance.player_id = tp.player_id AND tp.approved = 1')
+ // join to the team
+ ->innerJoin(['t' => 'team'], 'tp.team_id = t.id')
+ // join to approved members of the team
+ ->innerJoin(['am' => 'team_player'], 'am.team_id = t.id AND am.approved = 1')
+ // join approved member's last
+ ->innerJoin(['al' => 'player_last'], 'al.id = am.player_id and al.vpn_local_address is not null')
+ // ensure the approved member has a vpn_local_address
+ ->distinct();
+ }
+
public function active()
{
- return $this->andWhere('[[ip]] IS NOT NULL')->andWhere('[[reboot]]!=2');
+ return $this->andWhere('target_instance.[[ip]] IS NOT NULL')->andWhere('target_instance.[[reboot]]!=2');
+ }
+
+ public function last_updated(int $seconds_ago = 1)
+ {
+ return $this->andWhere(['<', 'target_instance.[[updated_at]]', new \yii\db\Expression("NOW() - INTERVAL $seconds_ago SECOND")]);
}
- public function pending_action(int $minutes_ago = 60)
+ public function pending_action(int $seconds_ago = 1)
{
- if($minutes_ago !== 0)
- return $this->andWhere('[[ip]] IS NULL')->orWhere(['>', 'reboot', 0])->orWhere(['<', 'updated_at', new \yii\db\Expression("NOW() - INTERVAL $minutes_ago MINUTE")]);
- return $this->andWhere('[[ip]] IS NULL')->orWhere(['>', 'reboot', 0]);
+ return $this->addSelect([
+ 'target_instance.*',
+ 'reboot' => new \yii\db\Expression(
+ "IF(target_instance.updated_at < (NOW() - INTERVAL :seconds SECOND), 2, target_instance.reboot)",
+ [':seconds' => $seconds_ago]
+ ),
+ ])
+ ->andWhere([
+ 'or',
+ ['target_instance.ip' => null],
+ ['>', 'target_instance.reboot', 0],
+ ['<', 'target_instance.updated_at', new \yii\db\Expression("NOW() - INTERVAL $seconds_ago SECOND")]
+ ]);
}
From abe52d67d37e0c23f514cb85fd60d60a6a531fd9 Mon Sep 17 00:00:00 2001
From: Pantelis Roditis
Date: Fri, 23 Jan 2026 19:24:04 +0200
Subject: [PATCH 30/71] dont apply timezone twice
---
backend/modules/settings/models/Sysconfig.php | 8 +++-----
1 file changed, 3 insertions(+), 5 deletions(-)
diff --git a/backend/modules/settings/models/Sysconfig.php b/backend/modules/settings/models/Sysconfig.php
index 021aad19f..7d01d8555 100644
--- a/backend/modules/settings/models/Sysconfig.php
+++ b/backend/modules/settings/models/Sysconfig.php
@@ -56,7 +56,7 @@ public function afterFind()
if ($this->val == 0 || $this->val == "")
$this->val = "";
else
- $this->val = Yii::$app->formatter->asDatetime($this->val, 'php:Y-m-d H:i:s', 'UTC');
+ $this->val = date('Y-m-d H:i:s', $this->val);
break;
default:
break;
@@ -101,11 +101,9 @@ public function afterSave($insert, $changedAttributes)
if ($this->id === 'stripe_webhookLocalEndpoint' && array_key_exists('val', $changedAttributes)) {
$oldVal = $changedAttributes['val'];
$newVal = $this->val;
- if(($u=UrlRoute::findOne(['destination'=>'subscription/default/webhook']))!==NULL)
- {
- $u->updateAttributes(['source'=>$newVal]);
+ if (($u = UrlRoute::findOne(['destination' => 'subscription/default/webhook'])) !== NULL) {
+ $u->updateAttributes(['source' => $newVal]);
}
-
}
}
From 0c552eb380cd3662c64afc9d2dd603062363a05d Mon Sep 17 00:00:00 2001
From: Pantelis Roditis
Date: Fri, 23 Jan 2026 19:27:48 +0200
Subject: [PATCH 31/71] update some default migration settings
---
.../m000000_000001_system_settings.php | 30 ++++++++++++++++---
1 file changed, 26 insertions(+), 4 deletions(-)
diff --git a/contrib/sample-migrations/m000000_000001_system_settings.php b/contrib/sample-migrations/m000000_000001_system_settings.php
index 5b76f0a90..ff7323611 100644
--- a/contrib/sample-migrations/m000000_000001_system_settings.php
+++ b/contrib/sample-migrations/m000000_000001_system_settings.php
@@ -31,6 +31,10 @@ class m000000_000001_system_settings extends Migration
['id' => "leaderboard_show_zero", 'val' => "0"],
['id' => "leaderboard_visible_after_event_end", 'val' => "1"],
['id' => "leaderboard_visible_before_event_start", 'val' => "0"],
+ ['id' => "country_rankings", 'val' => "0"],
+ ['id' => "player_point_rankings", 'val' => "0"],
+ ['id' => "player_monthly_rankings", 'val' => "0"],
+
['id' => 'frontpage_scenario', 'val' => 'Welcome to our lovely event... Edit from backend Content => Frontpage Scenario'],
['id' => "event_end_notification_title", 'val' => "🎉 Our awesome echoCTF finished 🎉"],
['id' => "event_end_notification_body", 'val' => "The awesome echoCTF is over 🎉🎉🎉 Congratulations to you and your team 👏👏👏 Thank you for participating!!!"],
@@ -59,23 +63,38 @@ class m000000_000001_system_settings extends Migration
['id' => "team_manage_members", 'val' => "1"],
['id' => "team_required", 'val' => "1"],
['id' => 'team_visible_instances', 'val' => "1"],
+ ['id' => 'team_only_leaderboards', 'val' => "1"],
+ ['id' => 'team_encrypted_claims_allowed', 'val' => "1"],
+
/**
* Player settings
*/
['id' => "approved_avatar", 'val' => "1"],
['id' => "player_profile", 'val' => "1"],
['id' => "profile_visibility", 'val' => "public"],
- ['id' => "require_activation", 'val' => "0"],
- ['id' => 'player_require_identification', 'val' => "0"],
+ ['id' => "require_activation", 'val' => "1"],
+ ['id' => 'player_require_identification', 'val' => "1"],
['id' => 'all_players_vip', 'val' => "1"],
- ['id' => 'player_require_approval', 'val' => "0"],
+ ['id' => 'player_require_approval', 'val' => "1"],
['id' => 'profile_discord', 'val' => "1"],
['id' => 'profile_echoctf', 'val' => "1"],
['id' => 'profile_github', 'val' => "1"],
['id' => 'profile_settings_fields', 'val' => 'avatar,bio,country,discord,echoctf,email,fullname,github,pending_progress,twitter,username,visibility'],
+ ['id' => 'avatar_robohash_set', 'val' => 'set3'],
+
/**
* Configuration settings
*/
+ ['id' => 'target_guest_view_deny', 'val' => '1'],
+ ['id' => 'disable_ondemand_operations', 'val' => '1'],
+ ['id' => 'module_smartcity_disabled', 'val' => '1'],
+ ['id' => 'module_speedprogramming_enabled', 'val' => '0'],
+ ['id' => 'dashboard_news_total_pages', 'val' => '10'],
+ ['id' => 'dashboard_news_records_per_page', 'val' => '3'],
+ ['id' => 'force_https_urls', 'val' => '1'],
+ ['id' => 'subscriptions_menu_show', 'val' => '0'],
+ ['id' => 'log_failed_claims', 'val' => '1'],
+
['id' => 'academic_grouping', 'val' => '0'],
['id' => "challenge_home", 'val' => "uploads/"],
['id' => "dashboard_is_home", 'val' => "1"],
@@ -126,8 +145,11 @@ class m000000_000001_system_settings extends Migration
*/
public function safeUp()
{
- foreach ($this->news as $entry)
+ foreach ($this->news as $entry) {
+ $entry['created_at']=new \yii\db\Expression('NOW()');
+ $entry['updated_at']=new \yii\db\Expression('NOW()');
$this->upsert('news', $entry, true);
+ }
// delete not needed url routes
foreach ($this->delete_url_routes as $route) {
From d659c7978b2dbb4caee2e0daa3382b53d213d06f Mon Sep 17 00:00:00 2001
From: Pantelis Roditis
Date: Fri, 23 Jan 2026 19:28:11 +0200
Subject: [PATCH 32/71] private_network and private_network_targets is needed
---
contrib/findingsd-federated.sql | 27 +++++++++++++++++++++++++++
1 file changed, 27 insertions(+)
diff --git a/contrib/findingsd-federated.sql b/contrib/findingsd-federated.sql
index ac873b622..7456e9f2a 100644
--- a/contrib/findingsd-federated.sql
+++ b/contrib/findingsd-federated.sql
@@ -116,6 +116,33 @@ CREATE TABLE `player_ssl` (
UNIQUE KEY `serial` (`serial`)
) ENGINE=FEDERATED DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci CONNECTION='mysql://{{db_user}}:{{db_pass}}@{{db_host}}:3306/{{db_name}}/player_ssl';
+DROP TABLE IF EXISTS `private_network`;
+CREATE TABLE `private_network` (
+ `id` int(11) NOT NULL AUTO_INCREMENT,
+ `player_id` int(11) unsigned DEFAULT NULL,
+ `name` varchar(255) DEFAULT NULL,
+ `team_accessible` tinyint(1) DEFAULT NULL,
+ `created_at` datetime DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ KEY `idx-private_network-player_id` (`player_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci CONNECTION='mysql://{{db_user}}:{{db_pass}}@{{db_host}}:3306/{{db_name}}/private_network';
+
+DROP TABLE IF EXISTS `private_network_target`;
+CREATE TABLE `private_network_target` (
+ `id` int(11) NOT NULL AUTO_INCREMENT,
+ `private_network_id` int(11) DEFAULT NULL,
+ `target_id` int(11) NOT NULL,
+ `ip` int(11) unsigned DEFAULT NULL,
+ `state` smallint(6) unsigned DEFAULT 0,
+ `server_id` int(11) DEFAULT NULL,
+ `ipoctet` varchar(15) GENERATED ALWAYS AS (inet_ntoa(`ip`)) VIRTUAL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `idx-unique-private_network_id-target_id` (`private_network_id`,`target_id`),
+ KEY `idx-private_network_target-private_network_id` (`private_network_id`),
+ KEY `idx-private_network_target-server_id` (`server_id`),
+ KEY `idx-private_network_target-target_id` (`target_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci CONNECTION='mysql://{{db_user}}:{{db_pass}}@{{db_host}}:3306/{{db_name}}/private_network_target';
+
DROP TABLE IF EXISTS `debuglogs`;
CREATE TABLE debuglogs (
From 6161138754bf01a9e12eda040e31ac4c1245cda3 Mon Sep 17 00:00:00 2001
From: Pantelis Roditis
Date: Fri, 23 Jan 2026 19:28:49 +0200
Subject: [PATCH 33/71] update triggers to match
---
schemas/echoCTF-triggers.sql | 207 +++++++++++++++++++----------------
1 file changed, 111 insertions(+), 96 deletions(-)
diff --git a/schemas/echoCTF-triggers.sql b/schemas/echoCTF-triggers.sql
index b558677d4..6f70a6f55 100644
--- a/schemas/echoCTF-triggers.sql
+++ b/schemas/echoCTF-triggers.sql
@@ -127,38 +127,49 @@ CREATE TRIGGER `tbi_headshot` BEFORE INSERT ON `headshot` FOR EACH ROW
END IF;
END ;;
-DROP TRIGGER IF EXISTS tai_headshot ;;
+DROP TRIGGER IF EXISTS `tai_headshot` ;;
CREATE TRIGGER `tai_headshot` AFTER INSERT ON `headshot` FOR EACH ROW
- thisBegin:BEGIN
- DECLARE private_instance int;
- IF (@TRIGGER_CHECKS = FALSE) THEN
- LEAVE thisBegin;
- END IF;
- SET private_instance=(SELECT COUNT(*) FROM target_instance WHERE player_id=NEW.player_id AND target_id=NEW.target_id);
- IF (SELECT headshot_spin FROM target WHERE id=NEW.target_id)>0 AND private_instance<1 THEN
- INSERT IGNORE INTO spin_queue (target_id, player_id,created_at) VALUES (NEW.target_id,NEW.player_id,NOW());
- ELSEIF private_instance>0 THEN
- UPDATE target_instance SET reboot=2 WHERE player_id=NEW.player_id AND target_id=NEW.target_id;
- END IF;
- IF (SELECT count(*) FROM target_ondemand WHERE target_id=NEW.target_id AND state=1)>0 THEN
- UPDATE target_ondemand SET heartbeat=(NOW() - INTERVAL 59 MINUTE - INTERVAL 30 SECOND) WHERE target_id=NEW.target_id;
- END IF;
- UPDATE target_state SET total_headshots=total_headshots+1,timer_avg=(SELECT ifnull(round(avg(timer)),0) FROM headshot WHERE target_id=NEW.target_id) WHERE id=NEW.target_id;
- END ;;
+ thisBegin:BEGIN
+ DECLARE private_instance int;
+ DECLARE local_points int;
+ DECLARE lheadshot_points int;
+ DECLARE lfirst_headshot_points int;
+ IF (@TRIGGER_CHECKS = FALSE) THEN
+ LEAVE thisBegin;
+ END IF;
+ SET local_points=0;
+ SELECT headshot_points,first_headshot_points INTO lheadshot_points,lfirst_headshot_points FROM target WHERE id=NEW.target_id;
+ SET private_instance=(SELECT COUNT(*) FROM target_instance WHERE player_id=NEW.player_id AND target_id=NEW.target_id);
+ IF (SELECT headshot_spin FROM target WHERE id=NEW.target_id)>0 AND private_instance<1 THEN
+ INSERT IGNORE INTO spin_queue (target_id, player_id,created_at) VALUES (NEW.target_id,NEW.player_id,NOW());
+ ELSEIF private_instance>0 THEN
+ UPDATE target_instance SET reboot=2 WHERE player_id=NEW.player_id AND target_id=NEW.target_id;
+ END IF;
+ IF (SELECT count(*) FROM target_ondemand WHERE target_id=NEW.target_id AND state=1)>0 THEN
+ UPDATE target_ondemand SET heartbeat=(NOW() - INTERVAL 59 MINUTE - INTERVAL 30 SECOND) WHERE target_id=NEW.target_id;
+ END IF;
+ IF NEW.first = 1 AND lfirst_headshot_points>0 THEN
+ SET local_points=lfirst_headshot_points;
+ ELSEIF lheadshot_points>0 THEN
+ SET local_points=lheadshot_points;
+ END IF;
+ INSERT INTO stream (player_id,model,model_id,points,title,message,pubtitle,pubmessage,ts) VALUES (NEW.player_id,'headshot',NEW.target_id,local_points,'','','','',now());
+ UPDATE target_state SET total_headshots=total_headshots+1,timer_avg=(SELECT ifnull(round(avg(timer)),0) FROM headshot WHERE target_id=NEW.target_id) WHERE id=NEW.target_id;
+ END ;;
DROP TRIGGER IF EXISTS tau_headshot ;;
CREATE TRIGGER `tau_headshot` AFTER UPDATE ON `headshot` FOR EACH ROW
- thisBegin:BEGIN
- IF (@TRIGGER_CHECKS = FALSE) THEN
- LEAVE thisBegin;
- END IF;
+thisBegin:BEGIN
+ IF (@TRIGGER_CHECKS = FALSE) THEN
+ LEAVE thisBegin;
+ END IF;
IF (OLD.rating IS NULL AND NEW.rating IS NOT NULL) OR (OLD.rating IS NOT NULL and NEW.rating!=OLD.rating) THEN
UPDATE target_state SET player_rating=(SELECT round(avg(rating)) FROM headshot WHERE target_id=NEW.target_id AND rating>-1) WHERE id=NEW.target_id;
END IF;
IF (OLD.timer IS NULL AND NEW.timer IS NOT NULL) OR (OLD.timer IS NOT NULL AND NEW.timer!=OLD.timer) THEN
- UPDATE target_state SET timer_avg=(SELECT ifnull(round(avg(timer)),0) FROM headshot WHERE target_id=NEW.target_id and timer>60) WHERE id=NEW.target_id;
+ UPDATE target_state SET timer_avg=(SELECT ifnull(round(avg(timer)),0) FROM headshot WHERE target_id=NEW.target_id and timer>60) WHERE id=NEW.target_id;
END IF;
- END ;;
+ END ;;
DROP TRIGGER IF EXISTS tad_headshot ;;
CREATE TRIGGER `tad_headshot` AFTER DELETE ON `headshot` FOR EACH ROW
@@ -214,7 +225,7 @@ CREATE TRIGGER tai_player AFTER INSERT ON player FOR EACH ROW
DROP TRIGGER IF EXISTS tau_player ;;
CREATE TRIGGER `tau_player` AFTER UPDATE ON `player` FOR EACH ROW
- thisBegin:BEGIN
+thisBegin:BEGIN
DECLARE ltitle VARCHAR(30) DEFAULT 'Joined the platform';
IF (@TRIGGER_CHECKS = FALSE) THEN
LEAVE thisBegin;
@@ -249,23 +260,23 @@ CREATE TRIGGER `tau_player` AFTER UPDATE ON `player` FOR EACH ROW
ELSEIF NEW.status=10 AND (OLD.status=9 OR OLD.status=8) THEN
DELETE FROM player_token WHERE player_id=NEW.id AND `type`='email_verification';
END IF;
- END ;;
+END ;;
DROP TRIGGER IF EXISTS tbd_player ;;
CREATE TRIGGER `tbd_player` BEFORE DELETE ON `player` FOR EACH ROW
- thisBegin:BEGIN
- DECLARE tid INT default 0;
- IF (@TRIGGER_CHECKS = FALSE) THEN
- LEAVE thisBegin;
- END IF;
- SELECT id INTO tid FROM team where owner_id=OLD.id;
- IF tid > 0 THEN
- DELETE FROM team_score WHERE team_id=tid;
- END IF;
- DELETE FROM player_ssl WHERE player_id=OLD.id;
- DELETE FROM player_rank WHERE player_id=OLD.id;
+thisBegin:BEGIN
+ DECLARE tid INT default 0;
+ IF (@TRIGGER_CHECKS = FALSE) THEN
+ LEAVE thisBegin;
+ END IF;
+ SELECT id INTO tid FROM team where owner_id=OLD.id;
+ IF tid > 0 THEN
+ DELETE FROM team_score WHERE team_id=tid;
+ END IF;
+ DELETE FROM player_ssl WHERE player_id=OLD.id;
+ DELETE FROM player_rank WHERE player_id=OLD.id;
DELETE FROM headshot WHERE player_id=OLD.id;
- END ;;
+END ;;
DROP TRIGGER IF EXISTS tad_player ;;
CREATE TRIGGER `tad_player` AFTER DELETE ON `player` FOR EACH ROW
@@ -365,7 +376,7 @@ CREATE TRIGGER `tbi_player_finding` BEFORE INSERT ON `player_finding` FOR EACH R
DROP TRIGGER IF EXISTS tai_player_finding ;;
CREATE TRIGGER `tai_player_finding` AFTER INSERT ON `player_finding` FOR EACH ROW
- thisBegin:BEGIN
+thisBegin:BEGIN
DECLARE local_target_id INT;
DECLARE headshoted INT default null;
DECLARE min_finding,min_treasure,max_finding,max_treasure, max_val, min_val DATETIME;
@@ -391,7 +402,7 @@ CREATE TRIGGER `tai_player_finding` AFTER INSERT ON `player_finding` FOR EACH RO
INSERT INTO headshot (player_id,target_id,created_at,timer) VALUES (NEW.player_id,local_target_id,now(),UNIX_TIMESTAMP(max_val)-UNIX_TIMESTAMP(min_val));
END IF;
INSERT INTO target_player_state (id,player_id,player_findings,player_points,created_at,updated_at) VALUES (local_target_id,NEW.player_id,1,NEW.points,now(),now()) ON DUPLICATE KEY UPDATE player_findings=player_findings+values(player_findings),player_points=player_points+values(player_points),updated_at=now();
- END ;;
+END ;;
DROP TRIGGER IF EXISTS tad_player_finding ;;
CREATE TRIGGER `tad_player_finding` AFTER DELETE ON `player_finding` FOR EACH ROW
@@ -408,25 +419,25 @@ END ;;
DROP TRIGGER IF EXISTS tau_player_last ;;
CREATE TRIGGER `tau_player_last` AFTER UPDATE ON `player_last` FOR EACH ROW
- thisBegin:BEGIN
- IF (@TRIGGER_CHECKS = FALSE) THEN
- LEAVE thisBegin;
- END IF;
+thisBegin:BEGIN
+ IF (@TRIGGER_CHECKS = FALSE) THEN
+ LEAVE thisBegin;
+ END IF;
- IF (OLD.vpn_local_address IS NULL AND NEW.vpn_local_address IS NOT NULL) THEN
- DO memc_set(CONCAT('ovpn:',NEW.id),INET_NTOA(NEW.vpn_local_address));
- DO memc_set(CONCAT('ovpn:',INET_NTOA(NEW.vpn_local_address)),NEW.id);
- DO memc_set(CONCAT('ovpn_remote:',NEW.id),INET_NTOA(NEW.vpn_remote_address));
- ELSEIF (OLD.vpn_local_address IS NOT NULL AND NEW.vpn_local_address IS NULL) THEN
- DO memc_delete(CONCAT('ovpn:',NEW.id));
- DO memc_delete(CONCAT('ovpn_remote:',NEW.id));
- DO memc_delete(CONCAT('ovpn:',INET_NTOA(OLD.vpn_local_address)));
- END IF;
+ IF (OLD.vpn_local_address IS NULL AND NEW.vpn_local_address IS NOT NULL) THEN
+ DO memc_set(CONCAT('ovpn:',NEW.id),INET_NTOA(NEW.vpn_local_address));
+ DO memc_set(CONCAT('ovpn:',INET_NTOA(NEW.vpn_local_address)),NEW.id);
+ DO memc_set(CONCAT('ovpn_remote:',NEW.id),INET_NTOA(NEW.vpn_remote_address));
+ ELSEIF (OLD.vpn_local_address IS NOT NULL AND NEW.vpn_local_address IS NULL) THEN
+ DO memc_delete(CONCAT('ovpn:',NEW.id));
+ DO memc_delete(CONCAT('ovpn_remote:',NEW.id));
+ DO memc_delete(CONCAT('ovpn:',INET_NTOA(OLD.vpn_local_address)));
+ END IF;
- IF (OLD.vpn_local_address IS NULL AND NEW.vpn_local_address IS NOT NULL) OR (OLD.vpn_local_address IS NOT NULL AND NEW.vpn_local_address IS NOT NULL AND NEW.vpn_local_address!=OLD.vpn_local_address) THEN
- INSERT INTO `player_vpn_history` (`player_id`,`vpn_local_address`,`vpn_remote_address`) VALUES (NEW.id,NEW.vpn_local_address,NEW.vpn_remote_address);
- END IF;
- END ;;
+ IF (OLD.vpn_local_address IS NULL AND NEW.vpn_local_address IS NOT NULL) OR (OLD.vpn_local_address IS NOT NULL AND NEW.vpn_local_address IS NOT NULL AND NEW.vpn_local_address!=OLD.vpn_local_address) THEN
+ INSERT INTO `player_vpn_history` (`player_id`,`vpn_local_address`,`vpn_remote_address`) VALUES (NEW.id,NEW.vpn_local_address,NEW.vpn_remote_address);
+ END IF;
+END ;;
DROP TRIGGER IF EXISTS tai_player_question ;;
CREATE TRIGGER `tai_player_question` AFTER INSERT ON `player_question` FOR EACH ROW
@@ -533,35 +544,35 @@ CREATE TRIGGER `tbi_player_treasure` BEFORE INSERT ON `player_treasure` FOR EACH
DROP TRIGGER IF EXISTS tai_player_treasure ;;
CREATE TRIGGER `tai_player_treasure` AFTER INSERT ON `player_treasure` FOR EACH ROW
- thisBegin:BEGIN
- DECLARE local_target_id INT;
- DECLARE headshoted INT default null;
+thisBegin:BEGIN
+ DECLARE local_target_id INT;
+ DECLARE headshoted INT default null;
DECLARE tfindings,pfindings INT;
- DECLARE min_finding,min_treasure,max_finding,max_treasure, max_val, min_val DATETIME;
+ DECLARE min_finding,min_treasure,max_finding,max_treasure, max_val, min_val DATETIME;
- IF (@TRIGGER_CHECKS = FALSE) THEN
- LEAVE thisBegin;
- END IF;
+ IF (@TRIGGER_CHECKS = FALSE) THEN
+ LEAVE thisBegin;
+ END IF;
- CALL add_treasure_stream(NEW.player_id,'treasure',NEW.treasure_id,NEW.points);
- CALL add_player_treasure_hint(NEW.player_id,NEW.treasure_id);
+ CALL add_treasure_stream(NEW.player_id,'treasure',NEW.treasure_id,NEW.points);
+ CALL add_player_treasure_hint(NEW.player_id,NEW.treasure_id);
- SET local_target_id=(SELECT target_id FROM treasure WHERE id=NEW.treasure_id);
- SET headshoted=(select true as headshoted FROM target as t left join treasure as t2 on t2.target_id=t.id left join finding as t3 on t3.target_id=t.id LEFT JOIN player_treasure as t4 on t4.treasure_id=t2.id and t4.player_id=NEW.player_id left join player_finding as t5 on t5.finding_id=t3.id and t5.player_id=NEW.player_id WHERE t.id=local_target_id GROUP BY t.id HAVING count(distinct t2.id)=count(distinct t4.treasure_id) AND count(distinct t3.id)=count(distinct t5.finding_id));
+ SET local_target_id=(SELECT target_id FROM treasure WHERE id=NEW.treasure_id);
+ SET headshoted=(select true as headshoted FROM target as t left join treasure as t2 on t2.target_id=t.id left join finding as t3 on t3.target_id=t.id LEFT JOIN player_treasure as t4 on t4.treasure_id=t2.id and t4.player_id=NEW.player_id left join player_finding as t5 on t5.finding_id=t3.id and t5.player_id=NEW.player_id WHERE t.id=local_target_id GROUP BY t.id HAVING count(distinct t2.id)=count(distinct t4.treasure_id) AND count(distinct t3.id)=count(distinct t5.finding_id));
SET tfindings=(SELECT count(*) FROM finding WHERE target_id=local_target_id);
SET pfindings=(SELECT count(*) FROM player_finding WHERE player_id=NEW.player_id AND finding_id IN (SELECT id FROM finding WHERE target_id=local_target_id));
- IF headshoted IS NOT NULL THEN
- SELECT min(ts),max(ts) INTO min_finding,max_finding FROM player_finding WHERE player_id=NEW.player_id AND finding_id IN (SELECT id FROM finding WHERE target_id=local_target_id);
- SELECT min(ts),max(ts) INTO min_treasure,max_treasure FROM player_treasure WHERE player_id=NEW.player_id AND treasure_id IN (SELECT id FROM treasure WHERE target_id=local_target_id);
- SELECT GREATEST(max_finding, max_treasure), LEAST(min_finding, min_treasure) INTO max_val,min_val;
- INSERT INTO headshot (player_id,target_id,created_at,timer) VALUES (NEW.player_id,local_target_id,now(),UNIX_TIMESTAMP(max_val)-UNIX_TIMESTAMP(min_val));
- END IF;
- INSERT INTO target_player_state (id,player_id,player_treasures,player_points,created_at,updated_at) VALUES (local_target_id,NEW.player_id,1,NEW.points,now(),now()) ON DUPLICATE KEY UPDATE player_treasures=player_treasures+values(player_treasures),player_points=player_points+values(player_points),updated_at=now();
+ IF headshoted IS NOT NULL THEN
+ SELECT min(ts),max(ts) INTO min_finding,max_finding FROM player_finding WHERE player_id=NEW.player_id AND finding_id IN (SELECT id FROM finding WHERE target_id=local_target_id);
+ SELECT min(ts),max(ts) INTO min_treasure,max_treasure FROM player_treasure WHERE player_id=NEW.player_id AND treasure_id IN (SELECT id FROM treasure WHERE target_id=local_target_id);
+ SELECT GREATEST(max_finding, max_treasure), LEAST(min_finding, min_treasure) INTO max_val,min_val;
+ INSERT INTO headshot (player_id,target_id,created_at,timer) VALUES (NEW.player_id,local_target_id,now(),UNIX_TIMESTAMP(max_val)-UNIX_TIMESTAMP(min_val));
+ END IF;
+ INSERT INTO target_player_state (id,player_id,player_treasures,player_points,created_at,updated_at) VALUES (local_target_id,NEW.player_id,1,NEW.points,now(),now()) ON DUPLICATE KEY UPDATE player_treasures=player_treasures+values(player_treasures),player_points=player_points+values(player_points),updated_at=now();
IF tfindings > 0 AND pfindings = 0 THEN
INSERT INTO abuser (player_id,title,reason,model,model_id,created_at,updated_at) VALUES (NEW.player_id,'Claim flag before finding','tai_player_treasure','treasure',NEW.treasure_id,NOW(),NOW());
END IF;
- END ;;
+END ;;
DROP TRIGGER IF EXISTS tbi_profile ;;
CREATE TRIGGER `tbi_profile` BEFORE INSERT ON `profile` FOR EACH ROW
@@ -611,29 +622,29 @@ END ;;
DROP TRIGGER IF EXISTS tai_stream ;;
CREATE TRIGGER `tai_stream` AFTER INSERT ON `stream` FOR EACH ROW
- thisBegin:BEGIN
- DECLARE lteam_id INT;
- IF (@TRIGGER_CHECKS = FALSE) THEN
- LEAVE thisBegin;
- END IF;
- IF NEW.points>0 THEN
- INSERT INTO player_score (player_id,points) VALUES (NEW.player_id,NEW.points) ON DUPLICATE KEY UPDATE points=points+values(points);
- END IF;
- SELECT team_id INTO lteam_id FROM team_player WHERE player_id=NEW.player_id AND approved=1;
- IF lteam_id IS NOT NULL THEN
+thisBegin:BEGIN
+ DECLARE lteam_id INT;
+ IF (@TRIGGER_CHECKS = FALSE) THEN
+ LEAVE thisBegin;
+ END IF;
+ IF NEW.points>0 THEN
+ INSERT INTO player_score (player_id,points) VALUES (NEW.player_id,NEW.points) ON DUPLICATE KEY UPDATE points=points+values(points);
+ END IF;
+ SELECT team_id INTO lteam_id FROM team_player WHERE player_id=NEW.player_id AND approved=1;
+ IF lteam_id IS NOT NULL THEN
INSERT IGNORE INTO team_stream (stream_id,player_id,team_id,model,model_id,points,ts) VALUES (NEW.id,NEW.player_id,lteam_id,NEW.model,NEW.model_id,NEW.points,NEW.ts);
- END IF;
- END ;;
+ END IF;
+END ;;
DROP TRIGGER IF EXISTS tad_stream ;;
CREATE TRIGGER `tad_stream` AFTER DELETE ON `stream` FOR EACH ROW
- thisBegin:BEGIN
- IF (@TRIGGER_CHECKS = FALSE) THEN
- LEAVE thisBegin;
- END IF;
- INSERT INTO player_score (player_id,points) VALUES (OLD.player_id,-OLD.points) ON DUPLICATE KEY UPDATE points=if(points+values(points)<0,0,points+values(points));
+thisBegin:BEGIN
+ IF (@TRIGGER_CHECKS = FALSE) THEN
+ LEAVE thisBegin;
+ END IF;
+ INSERT INTO player_score (player_id,points) VALUES (OLD.player_id,-OLD.points) ON DUPLICATE KEY UPDATE points=if(points+values(points)<0,0,points+values(points));
DELETE FROM team_stream where stream_id=OLD.id;
- END ;;
+END ;;
DROP TRIGGER IF EXISTS tai_sysconfig ;;
CREATE TRIGGER `tai_sysconfig` AFTER INSERT ON `sysconfig` FOR EACH ROW
@@ -976,12 +987,16 @@ CREATE TRIGGER `tad_writeup` AFTER DELETE ON `writeup` FOR EACH ROW
DROP TRIGGER IF EXISTS tai_player_target_help ;;
CREATE TRIGGER tai_player_target_help AFTER INSERT ON player_target_help FOR EACH ROW
- thisBegin:BEGIN
+thisBegin:BEGIN
+ DECLARE stream_player_target_help INT;
IF (@TRIGGER_CHECKS = FALSE) THEN
LEAVE thisBegin;
END IF;
- INSERT INTO stream (player_id,model,model_id,points,title,message,pubtitle,pubmessage,ts) VALUES (NEW.player_id,'player_target_help',NEW.target_id,0,'','','','',now());
- END ;;
+ SET stream_player_target_help=memc_get('sysconfig:stream_player_target_help');
+ IF stream_player_target_help IS NOT NULL and stream_player_target_help=1 THEN
+ INSERT INTO stream (player_id,model,model_id,points,title,message,pubtitle,pubmessage,ts) VALUES (NEW.player_id,'player_target_help',NEW.target_id,0,'Activated writeups for {target}','','','',now());
+ END IF;
+END ;;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
From 98da78b159c7479203531638c1b38bc2dbccc3fe Mon Sep 17 00:00:00 2001
From: Pantelis Roditis
Date: Fri, 23 Jan 2026 19:29:04 +0200
Subject: [PATCH 34/71] update routines to match
---
schemas/echoCTF-routines.sql | 27 ++++++++++-----------------
1 file changed, 10 insertions(+), 17 deletions(-)
diff --git a/schemas/echoCTF-routines.sql b/schemas/echoCTF-routines.sql
index a289d74c9..7eb951c95 100644
--- a/schemas/echoCTF-routines.sql
+++ b/schemas/echoCTF-routines.sql
@@ -403,23 +403,16 @@ BEGIN
UPDATE team_score SET points=0 WHERE team_id=tid;
DELETE FROM team_stream WHERE team_id=tid;
INSERT INTO team_stream (stream_id,player_id,team_id,model,model_id,points,ts)
- SELECT id, player_id, tid, model, model_id, points, ts
- FROM (
- SELECT s.*,
- ROW_NUMBER() OVER (
- PARTITION BY model, model_id
- ORDER BY ts ASC, id ASC
- ) AS rn
- FROM stream s
- WHERE model != 'user'
- AND player_id IN (
- SELECT player_id
- FROM team_player
- WHERE team_id = tid AND approved=1
- )
- ) t
- WHERE rn = 1
- ORDER BY id, ts;
+ SELECT id, player_id, tid, model, model_id, points, ts
+ FROM (
+ SELECT s.*,
+ ROW_NUMBER() OVER (PARTITION BY model, model_id ORDER BY ts ASC, id ASC) AS rn
+ FROM stream s
+ WHERE model != 'user'
+ AND player_id IN (SELECT player_id FROM team_player WHERE team_id = tid AND approved=1)
+ ) t
+ WHERE rn = 1
+ ORDER BY id, ts;
IF `_rollback` THEN
ROLLBACK;
ELSE
From fca1e5190cb5af85ca0cb27d2059aee67b9f7a86 Mon Sep 17 00:00:00 2001
From: Pantelis Roditis
Date: Fri, 23 Jan 2026 19:30:14 +0200
Subject: [PATCH 35/71] if team_visible_instances or team_subscription show
only spawn team instance
---
.../targetcardactions/TargetCardActions.php | 27 ++++++++++---------
1 file changed, 15 insertions(+), 12 deletions(-)
diff --git a/frontend/widgets/targetcardactions/TargetCardActions.php b/frontend/widgets/targetcardactions/TargetCardActions.php
index a315e5b67..a34d0c071 100644
--- a/frontend/widgets/targetcardactions/TargetCardActions.php
+++ b/frontend/widgets/targetcardactions/TargetCardActions.php
@@ -92,7 +92,6 @@ public function prep_instance_actions()
'linkOptions' => $linkOptions
];
} elseif ($this->target_instance === NULL) {
-
$this->target_actions[] = [
'label' => \Yii::t('app', ' Spawn a private instance'),
'url' => Url::to(['/target/default/spawn', 'id' => $this->model->id]),
@@ -100,16 +99,20 @@ public function prep_instance_actions()
'linkOptions' => ArrayHelper::merge($this->linkOptions, ['data-confirm' => \Yii::t('app', 'You are about to spawn a private instance of this target. Once booted, this instance will only be accessible by you and its IP will become visible here.')])
];
// display start team instance
- if (\Yii::$app->user->identity->subscription !== null && \Yii::$app->user->identity->subscription->active > 0 && \Yii::$app->user->identity->subscription->product !== null) {
- $metadata = json_decode(\Yii::$app->user->identity->subscription->product->metadata);
- if (isset($metadata->team_subscription) && boolval($metadata->team_subscription) === true) {
- $this->target_actions[] = [
- 'label' => \Yii::t('app', ' Spawn a team instance'),
- 'url' => Url::to(['/target/default/spawn', 'id' => $this->model->id, 'team' => true]),
- 'options' => ['style' => 'white-space: nowrap;'],
- 'linkOptions' => ArrayHelper::merge($this->linkOptions, ['data-confirm' => \Yii::t('app', 'You are about to spawn an instance of this target for your entire team. Once booted, this instance will only be accessible by you and your team. The IP will become visible here.')])
- ];
- }
+ if (
+ \Yii::$app->sys->team_visible_instances === true || (\Yii::$app->user->identity->subscription !== null
+ && \Yii::$app->user->identity->subscription->active > 0
+ && \Yii::$app->user->identity->subscription->product !== null
+ && ($metadata = json_decode(\Yii::$app->user->identity->subscription->product->metadata) !== null
+ && isset($metadata->team_subscription) && boolval($metadata->team_subscription) === true))
+ ) {
+ $key = \Yii::$app->sys->team_visible_instances === true ? 0 : null;
+ $this->target_actions[$key] = [
+ 'label' => \Yii::t('app', ' Spawn a team instance'),
+ 'url' => Url::to(['/target/default/spawn', 'id' => $this->model->id, 'team' => true]),
+ 'options' => ['style' => 'white-space: nowrap;'],
+ 'linkOptions' => ArrayHelper::merge($this->linkOptions, ['data-confirm' => \Yii::t('app', 'You are about to spawn an instance of this target for your entire team. Once booted, this instance will only be accessible by you and your team. The IP will become visible here.')])
+ ];
}
} elseif ($this->target_instance->target_id !== $this->model->id) {
$this->target_actions[] = [
@@ -168,7 +171,7 @@ public function prep_private_network_target_actions()
if ($this->model->private_network_id === null)
return;
- if(\app\modules\network\models\PrivateNetwork::findOne(['id'=>$this->model->private_network_id,'player_id'=>Yii::$app->user->id])!==NULL)
+ if (\app\modules\network\models\PrivateNetwork::findOne(['id' => $this->model->private_network_id, 'player_id' => Yii::$app->user->id]) !== NULL)
if ($this->model->ipoctet !== '0.0.0.0' && $this->model->ipoctet !== NULL && intval($this->model->state) === 0) {
$this->target_actions[] = [
'label' => \Yii::t('app', ' Reboot target'),
From 2c85024768d57830e1da349fd13b87307a49782c Mon Sep 17 00:00:00 2001
From: Pantelis Roditis
Date: Fri, 23 Jan 2026 19:30:47 +0200
Subject: [PATCH 36/71] use inviteOrCreate instead
---
.../material/modules/team/views/default/_team_card.php | 6 +++---
.../themes/material/modules/team/views/default/view.php | 8 ++++----
2 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/frontend/themes/material/modules/team/views/default/_team_card.php b/frontend/themes/material/modules/team/views/default/_team_card.php
index 08939f0de..8b540334c 100644
--- a/frontend/themes/material/modules/team/views/default/_team_card.php
+++ b/frontend/themes/material/modules/team/views/default/_team_card.php
@@ -89,9 +89,9 @@
user->identity->isAdmin || (Yii::$app->user->identity->team && !$model->inviteonly || ($invite === false && $listing === true))) : ?>
= Html::a('View Team', ['/team/default/view', 'token' => $model->token], ['class' => 'btn block text-dark text-bold orbitron' . (!$model->inviteonly ? ' btn-info' : ' btn-warning')]) ?>
- getTeamPlayers()->count()) < Yii::$app->sys->members_per_team && !Yii::$app->user->identity->team && !$model->locked && $model->invite) : ?>
- = Html::a('Join Team', ['/team/default/join', 'token' => $model->invite->token], ['class' => 'btn block btn-primary text-dark text-bold orbitron', 'data-method' => 'POST', 'data' => ['confirm' => 'You are about to join this team. Your membership will have to be confirmed by the team captain.', 'method' => 'POST']]) ?>
+ getTeamPlayers()->count()) < Yii::$app->sys->members_per_team && !Yii::$app->user->identity->team && !$model->locked && $model->inviteOrCreate) : ?>
+ = Html::a('Join Team', ['/team/default/join', 'token' => $model->inviteOrCreate->token], ['class' => 'btn block btn-primary text-dark text-bold orbitron', 'data-method' => 'POST', 'data' => ['confirm' => 'You are about to join this team. Your membership will have to be confirmed by the team captain.', 'method' => 'POST']]) ?>
-
\ No newline at end of file
+
diff --git a/frontend/themes/material/modules/team/views/default/view.php b/frontend/themes/material/modules/team/views/default/view.php
index 23e71ed62..4a34bd3be 100644
--- a/frontend/themes/material/modules/team/views/default/view.php
+++ b/frontend/themes/material/modules/team/views/default/view.php
@@ -19,10 +19,10 @@
= \Yii::t('app', 'Details for team') ?> [= Html::encode($team->name) ?>]
getTeamPlayers()->count() < Yii::$app->sys->members_per_team): ?>
- owner_id === Yii::$app->user->id || ($team->invite && !$team->inviteonly)): ?>
+ owner_id === Yii::$app->user->id || ($team->inviteOrCreate && !$team->inviteonly)): ?>
owner_id === Yii::$app->user->id) $class .= ' copy-to-clipboard'; ?>
= \Yii::t('app', 'Allow other players to join the team easily by providing them with this link:') ?>
- = Html::a(Url::to(['/team/default/invite', 'token' => $team->invite->token], 'https'), Url::to(['/team/default/invite', 'token' => $team->invite->token], 'https'), ['class' => $class, 'swal-data' => 'Copied to clipboard!']); ?>
+ = Html::a(Url::to(['/team/default/invite', 'token' => $team->inviteOrCreate->token], 'https'), Url::to(['/team/default/invite', 'token' => $team->inviteOrCreate->token], 'https'), ['class' => $class, 'swal-data' => 'Copied to clipboard!']); ?>
= Html::encode($team->recruitment) ?>
@@ -179,8 +179,8 @@
+registerJs(
+ 'hljs.highlightAll();',
+ $this::POS_READY,
+ 'markdown-highlighter'
+);
+?>
From fe45c55896197faae7ba57cd4f29f5e593d4b0a2 Mon Sep 17 00:00:00 2001
From: Pantelis Roditis
Date: Mon, 26 Jan 2026 13:48:13 +0200
Subject: [PATCH 71/71] document websockets a bit
---
docs/Websockets.md | 59 ++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 59 insertions(+)
create mode 100644 docs/Websockets.md
diff --git a/docs/Websockets.md b/docs/Websockets.md
new file mode 100644
index 000000000..3b46b4756
--- /dev/null
+++ b/docs/Websockets.md
@@ -0,0 +1,59 @@
+# Websockets service
+
+echoCTF.RED provides player updates to the live players through the use of [ws-server](https://github.com/echoCTF/ws-server).
+
+The services that want to communicate an update to the current live players submit their events through the HTTP service of ws-server.
+
+The system can send messages to a specific player or all connected players through the `/publish` and `/broadcast` endpoints respectively.
+
+Currently the following events are implemented:
+
+* `notification`: Sends a direct notification, Alert or Sweetalerts.
+* `apiNotifications`: Tell the clients to perform an update of their in-page notifications.
+* `target`: Update the target card if currently visible
+
+## Examples
+
+* Notify all users to perform an `apiNotifications()` js call. Effectively fetch the latest notifications through ajax.
+
+```shell
+curl -X POST "http://localhost:8888/broadcast" \
+ -H "Authorization: Bearer YOURTOKEN" \
+ -H "Content-Type: application/json" \
+ -d '{ "event": "apiNotifications" }'
+```
+
+* Send a SweetAlert (`"type": "swap:info"`) notification to player with id `1`
+
+```shell
+curl -X POST "http://localhost:8888/publish" \
+ -H "Authorization: Bearer server123token" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "player_id": "1",
+ "event": "notification",
+ "payload":
+ {
+ "title": "This is a notification",
+ "body": "This is the notification body",
+ "type": "swal:info"
+ }
+ }'
+```
+
+Note: Removing the `swal:` prefix from `type` sends a normal bootstrap alert notification.
+
+* Send an update for to player id `1` for updates on target id `2`
+
+```shell
+curl -X POST "http://localhost:8888/publish" \
+ -H "Authorization: Bearer server123token" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "player_id": "1",
+ "event": "target",
+ "payload": { "id": "2" }
+ }'
+```
+
+This will execute the js code `targetUpdates(2)`.