├── .devcontainer ├── Dockerfile ├── devcontainer.json ├── docker-compose.yml └── postInstall.sh ├── .dockerignore ├── .editorconfig ├── .env.example ├── .gitignore ├── .gitlab-ci.yml ├── .gitmodules ├── .idea ├── .gitignore ├── codeStyles │ ├── .gitignore │ └── Project.xml ├── encodings.xml ├── inspectionProfiles │ └── Project_Default.xml ├── markdown-exported-files.xml ├── markdown-navigator.xml ├── markdown-navigator │ └── profiles_settings.xml ├── misc.xml ├── modules.xml ├── saarctf.iml ├── sqldialects.xml ├── vcs.xml └── watcherTasks.xml ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── alembic.ini ├── checker_runner ├── __init__.py ├── celery_cmd.py ├── checker_execution.py ├── demo_checker.py ├── package_loader.py ├── runner.py ├── runners │ ├── __init__.py │ ├── eno.py │ ├── factory.py │ └── saarctf.py └── user_agents.py ├── ci ├── configure_caching.sh ├── ctf.py ├── install_dependencies.sh ├── rabbitmq.conf ├── test-demo-checker.py ├── test-flag-submission.sh ├── test-python-unittest.sh └── vpn.sh ├── config.sample.json ├── config.sample.yaml ├── controlserver ├── __init__.py ├── app.py ├── db_filesystem.py ├── dispatcher.py ├── endpoints │ ├── __init__.py │ ├── api.py │ ├── checker_results.py │ ├── flags.py │ ├── log_messages.py │ ├── metrics.py │ ├── pages.py │ ├── patches.py │ ├── services.py │ ├── teams.py │ └── utils.py ├── events.py ├── events_impl.py ├── flag_id_file.py ├── logger.py ├── master_timer.py ├── model_admin.py ├── models.py ├── patch_utils.py ├── scoring │ ├── __init__.py │ ├── algorithms │ │ ├── algorithm.py │ │ ├── factory.py │ │ ├── playground.py │ │ └── saarctf.py │ ├── scoreboard.py │ ├── scoreboard_process.py │ └── scoring.py ├── service_mgr.py ├── static │ ├── .gitignore │ ├── css │ │ ├── .gitignore │ │ └── vpnboard.css │ ├── img │ │ ├── favicon.png │ │ └── profile_dummy.png │ ├── js │ │ ├── general.js │ │ ├── graphs.js │ │ ├── overview.js │ │ ├── packages.js │ │ └── pagination.js │ └── less │ │ ├── color.less │ │ ├── index.less │ │ └── theme.less ├── templates │ ├── 404.html │ ├── admin_layout.html │ ├── base.html │ ├── checker_results.html │ ├── checker_results_view.html │ ├── checker_status.html │ ├── checker_status_overview.html │ ├── dashboard │ │ ├── panel-components.html │ │ ├── panel-flower.html │ │ ├── panel-logs.html │ │ ├── panel-timing.html │ │ └── panel-vpn.html │ ├── flags.html │ ├── index.html │ ├── log_messages.html │ ├── log_messages_view.html │ ├── packages.html │ ├── pagination.html │ ├── patches.html │ ├── scoreboard │ │ ├── index.html │ │ └── vpn.html │ ├── services.html │ ├── teams.html │ └── teams_view.html ├── timer.py ├── utils │ └── import_factory.py ├── vpncontrol.py └── wsgi.py ├── docker-compose.yml ├── flag-submission-server ├── .dockerignore ├── .gitignore ├── .idea │ ├── .gitignore │ ├── codeStyles │ │ ├── Project.xml │ │ └── codeStyleConfig.xml │ ├── flag-submission-server.iml │ ├── modules.xml │ └── vcs.xml ├── CMakeLists.txt ├── Dockerfile ├── README.md ├── benchmark │ ├── benchmark_newflags.cpp │ └── benchmark_oldflags.cpp ├── cmake │ ├── FindLibHiredis.cmake │ └── FindLibev.cmake ├── scripts │ ├── generate_bucket.py │ ├── generate_flag.py │ └── submit_new_flags.py ├── src │ ├── config.cpp │ ├── config.h │ ├── database.cpp │ ├── database.h │ ├── flagcache.cpp │ ├── flagcache.h │ ├── flagchecker.cpp │ ├── flagchecker.h │ ├── libraries │ │ ├── base64.c │ │ └── base64.h │ ├── main.cpp │ ├── periodic.cpp │ ├── periodic.h │ ├── redis.cpp │ ├── redis.h │ ├── statistics.cpp │ ├── statistics.h │ ├── windows_fixes │ │ ├── fixes.cpp │ │ └── hook.hpp │ ├── workerpool.cpp │ └── workerpool.h ├── stats.txt └── tests │ ├── test_main.cpp │ ├── testconfig.json │ ├── testconfig2.json │ └── testconfig3.json ├── migrations ├── alembic.ini ├── env.py ├── script.py.mako └── versions │ ├── 057792e3ba77_.py │ ├── 0e400cc111e2_.py │ ├── 10a5ee20d0ef_.py │ ├── 1418f098d06f_.py │ ├── 1eeeeb5988b0_.py │ ├── 251d7bd3641c_.py │ ├── 28250d12516f_.py │ ├── 2bcc2a3e63ea_.py │ ├── 2f793eb41f0e_.py │ ├── 4225a3d6edbd_add_timezone_info.py │ ├── 4a67c6f412ac_.py │ ├── 50067ab5c83d_enochecker.py │ ├── 55a062b550bf_.py │ ├── 5950403ab77d_add_ports_to_service_table.py │ ├── 5e6a03306c6e_.py │ ├── 6417a3f49101_.py │ ├── 65b6acc8e745_.py │ ├── 6a3f046884d6_.py │ ├── 7aab1b106d02_.py │ ├── 86dd27c83676_.py │ ├── 922cc6992093_add_wireguard_status_to_teams.py │ ├── 9676f8be1930_.py │ ├── a2e99d5c2195_.py │ ├── aea68a3e74a8_.py │ ├── b216b27a5b41_.py │ ├── b2b35103cfec_.py │ ├── b80816ee51bf_.py │ ├── bead29c0348f_.py │ ├── c2c1a5d3b062_.py │ ├── c84ad4bd6649_.py │ ├── d2f5aba10faa_record_tick_information_in_db.py │ ├── d3f46a7baef6_rename_round_to_tick.py │ ├── ddd894b2c20c_.py │ ├── e50efd6bd399_.py │ ├── e94f0d7b3dcc_.py │ ├── empty │ ├── f022f2589997_.py │ ├── f764e8e9fbb1_.py │ └── fd06fc8ae2cb_.py ├── mypy.ini ├── package.json ├── requirements-dev.txt ├── requirements-script.txt ├── requirements.txt ├── run-mypy.sh ├── run.sh ├── saarctf_commons ├── __init__.py ├── config.py ├── db_utils.py ├── debug_sql_timing.py ├── logging_utils.py ├── metric_utils.py └── redis.py ├── sample_files ├── Loadtest.md ├── checker_demoservice │ ├── checker_db.py │ └── checker_http.py ├── checker_fnf │ ├── checker.py │ └── test.py ├── checker_ok │ ├── checker.py │ └── test.py ├── demo_exploit.py ├── demo_traffic.py ├── demoservice.php ├── large_ctf.sql ├── randomflags.py ├── simple_exploit_runner.py ├── small_ctf.sql ├── submit-random-flags.py └── testdata.sql ├── scoreboard ├── .browserslistrc ├── .gitignore ├── README.md ├── angular.json ├── package-lock.json ├── package.json ├── proxy.conf.json ├── src │ ├── .htaccess │ ├── app │ │ ├── app-routing.module.ts │ │ ├── app.component.html │ │ ├── app.component.less │ │ ├── app.component.ts │ │ ├── app.module.ts │ │ ├── backend.service.spec.ts │ │ ├── backend.service.ts │ │ ├── chart-colorschemes.ts │ │ ├── current-tick │ │ │ ├── current-tick.component.html │ │ │ ├── current-tick.component.less │ │ │ ├── current-tick.component.spec.ts │ │ │ └── current-tick.component.ts │ │ ├── models.ts │ │ ├── notification-overlay │ │ │ ├── notification-overlay.component.html │ │ │ ├── notification-overlay.component.less │ │ │ ├── notification-overlay.component.spec.ts │ │ │ ├── notification-overlay.component.ts │ │ │ └── pyro.less │ │ ├── page-graphs │ │ │ ├── page-graphs.component.html │ │ │ ├── page-graphs.component.less │ │ │ ├── page-graphs.component.spec.ts │ │ │ └── page-graphs.component.ts │ │ ├── page-index │ │ │ ├── page-index.component.html │ │ │ ├── page-index.component.less │ │ │ ├── page-index.component.spec.ts │ │ │ └── page-index.component.ts │ │ ├── page-not-found │ │ │ ├── page-not-found.component.html │ │ │ ├── page-not-found.component.less │ │ │ ├── page-not-found.component.spec.ts │ │ │ └── page-not-found.component.ts │ │ ├── page-team │ │ │ ├── page-team.component.html │ │ │ ├── page-team.component.less │ │ │ ├── page-team.component.spec.ts │ │ │ └── page-team.component.ts │ │ ├── ratelimiter.ts │ │ ├── retryWithBackoff.ts │ │ ├── scoretable │ │ │ ├── scoretable.component.html │ │ │ ├── scoretable.component.less │ │ │ ├── scoretable.component.spec.ts │ │ │ └── scoretable.component.ts │ │ ├── settings │ │ │ ├── settings.component.html │ │ │ ├── settings.component.less │ │ │ ├── settings.component.spec.ts │ │ │ └── settings.component.ts │ │ ├── table-line-cells │ │ │ ├── table-line-cells.component.html │ │ │ ├── table-line-cells.component.less │ │ │ ├── table-line-cells.component.spec.ts │ │ │ └── table-line-cells.component.ts │ │ ├── table-service-header-cell │ │ │ ├── table-service-header-cell.component.html │ │ │ ├── table-service-header-cell.component.less │ │ │ ├── table-service-header-cell.component.spec.ts │ │ │ └── table-service-header-cell.component.ts │ │ ├── ui.service.spec.ts │ │ └── ui.service.ts │ ├── assets │ │ └── .gitkeep │ ├── darkmode.less │ ├── environments │ │ ├── environment.prod.ts │ │ └── environment.ts │ ├── favicon.png │ ├── index.html │ ├── main.ts │ ├── styles.less │ └── variables.less ├── tsconfig.app.json └── tsconfig.json ├── scripts ├── __init__.py ├── cloud_status_simple.py ├── export_ctftime_scoreboard.py ├── loadtest_service.py ├── manual_dispatch-collect.py ├── manual_dispatch.py ├── patch_prepare.py ├── patch_publish.py ├── recreate_firstblood.py ├── recreate_ranking.py ├── recreate_scoreboard.py ├── reset_ctf.py ├── reset_ctf_to_round.py ├── service_setup_scripts.py ├── service_update.py ├── sync_teams_http.py ├── test_dispatchspeed.py ├── worker_pool_increase.py └── worker_pool_status.py ├── tests ├── __init__.py ├── bench_scoring.py ├── test_config.py ├── test_dispatcher.py ├── test_gamelib.py ├── test_scoring.py ├── test_scripts.py ├── test_vpnboard.py └── utils │ ├── base_cases.py │ ├── celery.py │ └── scriptrunner.py ├── vpn ├── .gitignore ├── README.md ├── __init__.py ├── bpf │ ├── .gitignore │ ├── Makefile │ ├── anonymize_traffic.c │ ├── anonymize_traffic.o │ ├── bpf-utils.h │ ├── install-gameserver.sh │ ├── install.sh │ ├── traffic_marks.h │ ├── traffic_stats.c │ ├── traffic_stats.o │ ├── traffic_stats_gameserver.c │ ├── traffic_stats_gameserver.o │ └── uninstall.sh ├── build-openvpn-config-cloud.py ├── build-openvpn-config-oneperteam.py ├── build-openvpn-orga-multi.py ├── build-wireguard-orga-multi.py ├── iptables.sh ├── manage-hetzner-firewall.py ├── manage-iptables.py ├── manage-trafficstats.py ├── on-cloud-connect.py ├── on-cloud-disconnect.py ├── on-cloud-down.py ├── on-connect.py ├── on-connect.sh ├── on-device-up.sh ├── on-disconnect.py ├── on-disconnect.sh ├── ratelimit │ ├── install.sh │ └── reapply-all.sh ├── reset-connection-status.py ├── tcpdump │ ├── move-gametraffic.sh │ ├── move-teamtraffic.sh │ ├── run-tcpdump.sh │ └── setup.sh ├── test │ ├── Dockerfile │ ├── docker-compose.yml │ ├── test-client.py │ └── test-server.py ├── useful-commands.txt ├── vpnctl.py └── vpnlib.py ├── vpnboard ├── __init__.py ├── records.py ├── templates │ └── vpn.html ├── vpn_board.py ├── vpn_status_daemon.py ├── vpnchecks.py └── wg_status.py └── wireguard-sync ├── .env.example ├── README.md ├── docker ├── Dockerfile ├── README.md └── docker-compose.yml ├── poetry.lock ├── pyproject.toml └── wireguard_sync ├── __init__.py ├── __main__.py ├── exceptions.py ├── network_lib.py ├── rest_api.py ├── settings.py ├── test-api.json ├── test_network_lib.py └── utils.py /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/vscode-remote/devcontainer.json or the definition README at 2 | // https://github.com/microsoft/vscode-dev-containers/tree/master/containers/ubuntu-18.04-git 3 | { 4 | "name": "saarCTF Gameserver", 5 | "dockerComposeFile": "docker-compose.yml", 6 | "service": "saarctf-gameserver", 7 | "postCreateCommand": "./.devcontainer/postInstall.sh", 8 | "workspaceFolder": "/workspace", 9 | "settings": { 10 | "terminal.integrated.shell.linux": "/bin/bash", 11 | }, 12 | // Add the IDs of any extensions you want installed in the array below. 13 | "extensions": [ 14 | "bradymholt.pgformatter", 15 | "ckolkman.vscode-postgres", 16 | "codezombiech.gitignore", 17 | "davidanson.vscode-markdownlint", 18 | "ms-python.python", 19 | "ms-vscode.cpptools", 20 | "redhat.vscode-yaml", 21 | "twxs.cmake", 22 | "vector-of-bool.cmake-tools", 23 | "yzhang.markdown-all-in-one", 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /.devcontainer/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "2.2" 2 | 3 | services: 4 | saarctf-gameserver: 5 | build: 6 | context: "." 7 | dockerfile: Dockerfile 8 | volumes: 9 | - ..:/workspace:Z 10 | 11 | ports: 12 | - 5000:5000 13 | - 5555:5555 14 | - 31337:31337 15 | 16 | # Uncomment the next four lines if you will use a ptrace-based debugger like C++, Go, and Rust. 17 | # cap_add: 18 | # - SYS_PTRACE 19 | # security_opt: 20 | # - seccomp:unconfined 21 | 22 | user: "1000" 23 | # Overrides default command so things don't shut down after the process ends. 24 | command: sleep infinity 25 | 26 | postgres: 27 | image: postgres:10 28 | restart: on-failure 29 | environment: 30 | # https://hub.docker.com/_/postgres#how-to-extend-this-image 31 | POSTGRES_USER: saarsec 32 | POSTGRES_PASSWORD: 123456789 33 | POSTGRES_DB: saarctf 34 | redis: 35 | image: redis:5.0-alpine 36 | restart: on-failure 37 | rabbitmq: 38 | # If the rabbitmq container hangs with 100% CPU usage this might be caused by a too large value for `ulimit -n` 39 | image: rabbitmq:3.7-management-alpine 40 | restart: on-failure 41 | environment: 42 | # https://hub.docker.com/_/rabbitmq#setting-default-user-and-password 43 | RABBITMQ_DEFAULT_USER: saarsec 44 | RABBITMQ_DEFAULT_PASS: 123456789 45 | # https://hub.docker.com/_/rabbitmq#memory-limits 46 | RABBITMQ_VM_MEMORY_HIGH_WATERMARK: 100MiB 47 | RABBITMQ_DEFAULT_VHOST: saarctf 48 | -------------------------------------------------------------------------------- /.devcontainer/postInstall.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euxo pipefail 3 | 4 | # Configure database credentials for container setup 5 | cp ./config.containers.json ./config.json 6 | 7 | # Install additional dependencies 8 | python3 -m pip install -r requirements.txt 9 | python3 -m pip install -r script-requirements.txt 10 | npm install 11 | npm run build 12 | 13 | # Setup database schema 14 | flask db upgrade 15 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | venv 2 | .git 3 | **/.idea 4 | **/*.log 5 | **/__pycache__ 6 | **/node_modules 7 | **/cmake-build-* 8 | */build 9 | 10 | scoreboard/node_modules 11 | scoreboard/.angular 12 | scoreboard/dist 13 | controlserver/static/fonts 14 | controlserver/static/vendor 15 | config.* 16 | 17 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.py] 2 | indent_style = space 3 | indent_size = 4 4 | max_line_length = 150 5 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | POSTGRES_DB=saarctf 2 | POSTGRES_USER=saarctf 3 | POSTGRES_PASSWORD= 4 | RABBITMQ_DEFAULT_USER=saarctf 5 | RABBITMQ_DEFAULT_PASS= 6 | RABBITMQ_DEFAULT_VHOST=saarctf 7 | 8 | CHECKER_CONCURRENCY=16 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | *.pyc 3 | *.pyo 4 | /.idea 5 | /venv/ 6 | /wireguard-sync/venv/ 7 | /node_modules/ 8 | /package-lock.json 9 | /tmp/ 10 | /.mypy_cache/ 11 | config*.json 12 | config*.yaml 13 | **/.env 14 | **/.env.* 15 | !**/.env.example 16 | /wireguard-sync/keystore 17 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "gamelib"] 2 | path = gamelib 3 | url = ../saarctf-gamelib.git 4 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | /workspace.xml 2 | /dataSources.xml 3 | /dataSources/ 4 | /dataSources.local.xml 5 | -------------------------------------------------------------------------------- /.idea/codeStyles/.gitignore: -------------------------------------------------------------------------------- 1 | /codeStyleConfig.xml 2 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 30 | -------------------------------------------------------------------------------- /.idea/markdown-exported-files.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/markdown-navigator/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/saarctf.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 21 | 22 | -------------------------------------------------------------------------------- /.idea/sqldialects.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/watcherTasks.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:22 AS scoreboard_build 2 | 3 | RUN mkdir /opt/scoreboard /opt/controlserver 4 | WORKDIR /opt/scoreboard 5 | 6 | ADD scoreboard/package.json scoreboard/package-lock.json /opt/scoreboard/ 7 | RUN --mount=type=cache,target=/root/.npm \ 8 | npm install 9 | 10 | ADD scoreboard /opt/scoreboard 11 | ADD controlserver/static /opt/controlserver/static 12 | RUN npm run build 13 | 14 | 15 | FROM node:22 AS frontend_build 16 | WORKDIR /opt 17 | ADD package.json /opt/ 18 | RUN --mount=type=cache,target=/root/.npm \ 19 | npm install 20 | 21 | ADD controlserver/static /opt/controlserver/static 22 | RUN npm run build 23 | 24 | 25 | # the actual container with all python-based things 26 | FROM python:3.13 27 | WORKDIR /opt 28 | 29 | ADD requirements* /opt/ 30 | ADD Makefile /opt/ 31 | ADD gamelib /opt/gamelib 32 | 33 | RUN --mount=type=cache,target=/root/.cache \ 34 | make deps && \ 35 | . venv/bin/activate && \ 36 | pip install gunicorn && \ 37 | mkdir -p scoreboard 38 | 39 | ADD alembic.ini /opt/alembic.ini 40 | ADD checker_runner /opt/checker_runner 41 | ADD controlserver /opt/controlserver 42 | ADD migrations /opt/migrations 43 | ADD run.sh /opt/run.sh 44 | ADD saarctf_commons /opt/saarctf_commons 45 | ADD sample_files /opt/sample_files 46 | ADD scripts /opt/scripts 47 | ADD vpn /opt/vpn 48 | ADD vpnboard /opt/vpnboard 49 | ADD wireguard-sync /opt/wireguard-sync 50 | 51 | COPY --from=scoreboard_build /opt/scoreboard/dist /opt/scoreboard/dist 52 | COPY --from=frontend_build /opt/controlserver/static /opt/controlserver/static 53 | 54 | ENV FLASK_APP=controlserver/app.py 55 | STOPSIGNAL SIGINT 56 | 57 | ENTRYPOINT ["/opt/venv/bin/python"] 58 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Markus Bauer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | IN_VENV=. venv/bin/activate 3 | 4 | # create venv 5 | venv: 6 | python3 -m venv venv 7 | $(IN_VENV) ; pip install -U pip setuptools wheel 8 | 9 | # install dependencies 10 | .PHONY: deps 11 | deps: venv/deps 12 | venv/deps: venv requirements.txt requirements-dev.txt 13 | $(IN_VENV) ; pip install -Ur requirements.txt 14 | $(IN_VENV) ; pip install -Ur requirements-dev.txt 15 | @touch $(@) 16 | 17 | 18 | # install typical checker script dependencies 19 | deps-script: venv/deps venv/deps-script 20 | venv/deps-script: venv gamelib/checker-default-requirements.txt 21 | $(IN_VENV) ; pip install -Ur gamelib/checker-default-requirements.txt 22 | @touch $(@) 23 | 24 | 25 | # check project integrity 26 | check: check-mypy 27 | check-mypy: deps 28 | #TODO add --disallow-untyped-defs at some point 29 | $(IN_VENV) ; mypy --config-file mypy.ini --no-incremental checker_runner controlserver gamelib saarctf_commons scripts tests vpn vpnboard wireguard-sync/wireguard_sync 30 | 31 | 32 | # run all the unittests 33 | .PHONY: test 34 | test: deps deps-script 35 | $(IN_VENV) ; python3 -m unittest tests/test_*.py 36 | 37 | 38 | # cleanup everything including venv 39 | .PHONY: clean 40 | clean: 41 | rm -rf venv 42 | rm -rf .mypy_cache 43 | find . -ignore_readdir_race -type d -name __pycache__ -exec rm -rf {} \; 2>/dev/null || true 44 | -------------------------------------------------------------------------------- /alembic.ini: -------------------------------------------------------------------------------- 1 | migrations/alembic.ini -------------------------------------------------------------------------------- /checker_runner/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarkusBauer/saarctf-gameserver/88bee31ca3bf56492714eac5d0c6f96bc1a92079/checker_runner/__init__.py -------------------------------------------------------------------------------- /checker_runner/celery_cmd.py: -------------------------------------------------------------------------------- 1 | from saarctf_commons.config import load_default_config 2 | from saarctf_commons.redis import NamedRedisConnection 3 | from controlserver.models import init_database 4 | from checker_runner.user_agents import init_celery_environment 5 | from checker_runner.runner import celery_worker 6 | 7 | load_default_config() 8 | NamedRedisConnection.set_clientname('worker') 9 | init_database() 10 | init_celery_environment() 11 | celery_worker.init() 12 | celery = celery_worker.app 13 | -------------------------------------------------------------------------------- /checker_runner/checker_execution.py: -------------------------------------------------------------------------------- 1 | from abc import abstractmethod, ABC 2 | from dataclasses import dataclass, field 3 | 4 | _process_needs_restart = False 5 | 6 | 7 | def process_needs_restart() -> bool: 8 | global _process_needs_restart 9 | return _process_needs_restart 10 | 11 | 12 | def set_process_needs_restart() -> None: 13 | global _process_needs_restart 14 | _process_needs_restart = True 15 | 16 | 17 | @dataclass 18 | class CheckerRunOutput: 19 | status: str # "SUCCESS" etc 20 | output: str | None = None 21 | message: str | None = None 22 | data: dict = field(default_factory=dict) # additional, runner-specific data 23 | 24 | 25 | class CheckerRunner(ABC): 26 | def __init__(self, service_id: int, package: str, script: str, cfg: dict | None) -> None: 27 | """ 28 | :param service_id: 29 | :param package: 30 | :param script: Format: ":" 31 | :param cfg: config from database 32 | """ 33 | self.service_id = service_id 34 | self.package = package 35 | self.script = script 36 | self.cfg = cfg or {} 37 | 38 | @abstractmethod 39 | def execute_checker(self, team_id: int, tick: int) -> CheckerRunOutput: 40 | raise NotImplementedError 41 | 42 | def execute_checker_subprocess(self, team_id: int, tick: int, timeout: int) \ 43 | -> CheckerRunOutput: 44 | raise NotImplementedError 45 | -------------------------------------------------------------------------------- /checker_runner/runners/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarkusBauer/saarctf-gameserver/88bee31ca3bf56492714eac5d0c6f96bc1a92079/checker_runner/runners/__init__.py -------------------------------------------------------------------------------- /checker_runner/runners/factory.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from checker_runner.checker_execution import CheckerRunner 4 | from controlserver.utils.import_factory import ImportFactory 5 | 6 | 7 | class CheckerRunnerFactory(ImportFactory[CheckerRunner]): 8 | base_class = CheckerRunner 9 | 10 | @classmethod 11 | def build(cls, runner: str, service_id: int, package: str, script: str, cfg: dict | None, **kwargs: Any) -> CheckerRunner: 12 | return cls.get_class(runner or 'saarctf:SaarctfServiceRunner')(service_id, package, script, cfg, **kwargs) 13 | -------------------------------------------------------------------------------- /ci/configure_caching.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euxo pipefail 3 | 4 | # Check that the ulimit value is not too high as this is a problem for rabbitmq 5 | if [ "$(ulimit -n)" -gt 1100000 ] 6 | then 7 | echo "The ulimit for open file descriptors is very large:" "$(ulimit -n)" 8 | echo This may be a problem for rabbitmq and lead to hangs in the containers. 9 | exit 1 10 | fi 11 | 12 | # Ensure cache directories exist 13 | mkdir -p \ 14 | "$CI_PROJECT_DIR"/.cache/apt/lists/partial \ 15 | "$CI_PROJECT_DIR"/.cache/apt/archives/partial \ 16 | "$CI_PROJECT_DIR"/.cache/pip \ 17 | "$CI_PROJECT_DIR"/.cache/npm 18 | 19 | 20 | # Configure APT for caching 21 | echo "dir::state::lists $CI_PROJECT_DIR/.cache/apt/lists;" >> /etc/apt/apt.conf 22 | echo "dir::cache::archives $CI_PROJECT_DIR/.cache/apt/archives;" >> /etc/apt/apt.conf 23 | -------------------------------------------------------------------------------- /ci/ctf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | 5 | import requests 6 | 7 | URL = "http://127.0.0.1:5000/overview/set_timing" 8 | 9 | 10 | def start_ctf() -> None: 11 | requests.post(URL, json={"state": 3}, timeout=10).raise_for_status() 12 | 13 | 14 | def pause_ctf() -> None: 15 | requests.post(URL, json={"state": 2}, timeout=10).raise_for_status() 16 | 17 | 18 | def stop_ctf() -> None: 19 | requests.post(URL, json={"state": 1}, timeout=10).raise_for_status() 20 | 21 | 22 | def set_roundtime(roundtime: int) -> None: 23 | requests.post(URL, json={"roundtime": roundtime}, timeout=10).raise_for_status() 24 | 25 | 26 | def set_lastround(lastround: int) -> None: 27 | requests.post(URL, json={"lastround": lastround}, timeout=10).raise_for_status() 28 | 29 | 30 | def main() -> None: 31 | if sys.argv[1] == "start": 32 | start_ctf() 33 | elif sys.argv[1] == "pause": 34 | pause_ctf() 35 | elif sys.argv[1] == "stop": 36 | stop_ctf() 37 | elif sys.argv[1] == "roundtime": 38 | set_roundtime(int(sys.argv[2])) 39 | elif sys.argv[1] == "lastround": 40 | set_lastround(int(sys.argv[2])) 41 | else: 42 | print( 43 | f"Unknown flag: {sys.argv[1]}\nUse one of: start, pause, stop, roundtime, lastround" 44 | ) 45 | sys.exit(1) 46 | 47 | 48 | if __name__ == "__main__": 49 | main() 50 | -------------------------------------------------------------------------------- /ci/install_dependencies.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euxo pipefail 3 | 4 | # Install deb dependencies 5 | apt-get update 6 | apt-get install -y \ 7 | curl 8 | #curl -sL https://deb.nodesource.com/setup_12.x | bash 9 | apt-get -y install --no-install-recommends \ 10 | build-essential \ 11 | clang \ 12 | cmake \ 13 | g++ \ 14 | git \ 15 | iputils-ping \ 16 | libev-dev \ 17 | libhiredis-dev \ 18 | libpq-dev \ 19 | libssl-dev \ 20 | nodejs \ 21 | postgresql-client-17 \ 22 | postgresql-server-dev-all \ 23 | psmisc \ 24 | python3 \ 25 | python3-dev \ 26 | python3-pip \ 27 | python3-setuptools \ 28 | python3-venv \ 29 | python3-wheel \ 30 | python3-cryptography \ 31 | python3-redis python3-psycopg2 \ 32 | nodejs npm 33 | 34 | ln -s /usr/bin/nodejs /usr/local/bin/node 35 | 36 | # Install pip dependencies 37 | make deps deps-script 38 | 39 | # Install npm dependencies 40 | npm install 41 | npm run build 42 | # Install and build the scoreboard 43 | pushd scoreboard 44 | npm install 45 | npm run build 46 | popd 47 | -------------------------------------------------------------------------------- /ci/rabbitmq.conf: -------------------------------------------------------------------------------- 1 | loopback_users.guest = false 2 | 3 | # 128MB is not enough! 4 | #vm_memory_high_watermark.absolute = 512MiB 5 | 6 | listeners.tcp.default = 5672 7 | 8 | default_pass = 123456789 9 | 10 | default_user = saarsec 11 | 12 | default_vhost = saarctf 13 | 14 | management.tcp.port = 15672 15 | -------------------------------------------------------------------------------- /ci/test-flag-submission.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euxo pipefail 3 | 4 | # shellcheck source=/dev/null 5 | . venv/bin/activate 6 | 7 | # Configure database credentials for container setup 8 | cp ./config.containers.json ./config.json 9 | export FLASK_APP=controlserver/app.py 10 | alembic upgrade head 11 | flask run --host=0.0.0.0 & 12 | # Ensure flask is fully started by building something in the mean time 13 | 14 | # Build flag submission server 15 | mkdir flag-submission-server/build 16 | pushd flag-submission-server/build 17 | cmake -DCMAKE_BUILD_TYPE=Release -DPostgreSQL_ADDITIONAL_VERSIONS=17 .. 18 | make 19 | popd 20 | 21 | # Run some simple start/stop tests against the gameserver 22 | ./ci/ctf.py start 23 | sleep 3 24 | ./ci/ctf.py pause 25 | sleep 3 26 | ./ci/ctf.py start 27 | sleep 3 28 | ./ci/vpn.sh close 29 | sleep 3 30 | ./ci/vpn.sh open 31 | sleep 3 32 | ./ci/ctf.py stop 33 | echo "yes" | python3 ./scripts/reset_ctf.py 34 | 35 | # Start the gameserver and start the game 36 | ./ci/ctf.py start 37 | # Test flag submitter 38 | pushd flag-submission-server/build 39 | ./testsuite 40 | ./flag-submission-server > /tmp/submission.stdout 2>/tmp/submission.stderr & 41 | # SUBMISSION_SERVER_PID=$! 42 | sleep 1 43 | ./benchmark-newflags 44 | ./benchmark-oldflags 45 | # Check response of flag submission 46 | python3 ../scripts/submit_new_flags.py 10 47 | killall ./flag-submission-server 48 | echo "Submission Server Recorded Stdout:" 49 | cat /tmp/submission.stdout 50 | echo "Submission Server Recorded Stderr:" 51 | cat /tmp/submission.stderr 52 | popd 53 | echo "yes" | python3 ./scripts/reset_ctf.py 54 | -------------------------------------------------------------------------------- /ci/test-python-unittest.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euxo pipefail 3 | 4 | # Run static checker 5 | make check 6 | 7 | # Configure database credentials for container setup 8 | sed 's/"database": "saarctf"/"database": "saarctf_unittest"/' ./config.containers.json > ./config.test.json 9 | export PGPASSWORD=123456789 10 | echo 'CREATE DATABASE saarctf_unittest;' | psql -Usaarsec -h postgres saarctf 11 | 12 | make test 13 | -------------------------------------------------------------------------------- /ci/vpn.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | case "$1" in 3 | "open") 4 | curl 'http://127.0.0.1:5000/overview/set_vpn' --silent --show-error --compressed -H 'Content-Type: application/json;charset=utf-8' --data '{"state":"on"}' 5 | ;; 6 | "close") 7 | curl 'http://127.0.0.1:5000/overview/set_vpn' --silent --show-error --compressed -H 'Content-Type: application/json;charset=utf-8' --data '{"state":"off"}' 8 | ;; 9 | *) 10 | echo -e "Unkown argument.\nSpecify one of: open, close" 11 | exit 1 12 | ;; 13 | esac 14 | -------------------------------------------------------------------------------- /controlserver/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarkusBauer/saarctf-gameserver/88bee31ca3bf56492714eac5d0c6f96bc1a92079/controlserver/__init__.py -------------------------------------------------------------------------------- /controlserver/endpoints/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarkusBauer/saarctf-gameserver/88bee31ca3bf56492714eac5d0c6f96bc1a92079/controlserver/endpoints/__init__.py -------------------------------------------------------------------------------- /controlserver/endpoints/api.py: -------------------------------------------------------------------------------- 1 | """ 2 | API to interact with other programs 3 | """ 4 | from flask import Blueprint, request 5 | from flask.typing import ResponseReturnValue 6 | 7 | from controlserver.models import LogMessage 8 | from controlserver.logger import log 9 | 10 | app = Blueprint('api', __name__) 11 | 12 | 13 | @app.route('/api/grafana_warning', methods=['POST']) 14 | def api_grafana_warning() -> ResponseReturnValue: 15 | data = request.get_json() 16 | if not data: 17 | return 'Invalid', 500 18 | if data['state'] != 'alerting': 19 | return 'We only want alerts', 500 20 | if 'ruleName' in data: 21 | text = data['message'] + '\nRule: ' + data['ruleName'] + '\n' + data['ruleUrl'] 22 | log('Grafana', data['title'], text, LogMessage.WARNING) 23 | elif 'alerts' in data: 24 | for alert in data['alerts']: 25 | if alert['status'] != 'firing': 26 | continue 27 | title = '[Grafana] ' + alert['labels']['alertname'] 28 | if 'rulename' in alert['labels']: 29 | title += ' / ' + alert['labels']['rulename'] 30 | text = [] 31 | if 'dashboardURL' in alert and alert['dashboardURL']: 32 | text.append('Dashboard: ' + alert['dashboardURL']) 33 | if 'panelURL' in alert and alert['panelURL']: 34 | text.append('Panel: ' + alert['panelURL']) 35 | log('Grafana', title, '\n'.join(text), LogMessage.WARNING) 36 | else: 37 | text = data['title'] + '\n' + data['message'] 38 | log('Grafana', data['title'], text, LogMessage.WARNING) 39 | return 'OK' 40 | -------------------------------------------------------------------------------- /controlserver/endpoints/metrics.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, request 2 | from flask.typing import ResponseReturnValue 3 | from saarctf_commons.metric_utils import Metrics 4 | 5 | app = Blueprint('metrics', __name__) 6 | 7 | 8 | @app.route('/metrics/write', methods=['POST']) 9 | def metrics_write() -> ResponseReturnValue: 10 | data = request.get_json() 11 | for record in data: 12 | Metrics.record_many(record['metric'], record['values'], record.get('ts', None), **record.get('attributes', {})) 13 | return 'OK' 14 | -------------------------------------------------------------------------------- /controlserver/endpoints/services.py: -------------------------------------------------------------------------------- 1 | """ 2 | List Services 3 | """ 4 | from collections import defaultdict 5 | from flask import Blueprint, render_template, request 6 | from flask.typing import ResponseReturnValue 7 | 8 | from controlserver.models import Service, SubmittedFlag, db_session, expect 9 | 10 | app = Blueprint('services', __name__) 11 | 12 | 13 | @app.route('/services/', methods=['GET']) 14 | def services_index() -> ResponseReturnValue: 15 | services = Service.query.order_by(Service.id).all() 16 | 17 | first_bloods = defaultdict(list) 18 | for flag in SubmittedFlag.query.filter(SubmittedFlag.is_firstblood).order_by(SubmittedFlag.ts).all(): 19 | first_bloods[flag.service_id].append(flag) 20 | 21 | return render_template('services.html', services=services, first_bloods=first_bloods) 22 | 23 | 24 | @app.route('/services/checker_status', methods=['POST']) 25 | def services_set_checker_status() -> ResponseReturnValue: 26 | service: Service = expect(Service.query.get(request.form['id'])) 27 | if service: 28 | service.checker_enabled = request.form['status'] == '1' 29 | session = db_session() 30 | session.add(service) 31 | session.commit() 32 | return 'OK' 33 | else: 34 | return 'Not found' 35 | -------------------------------------------------------------------------------- /controlserver/endpoints/utils.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Iterable 3 | 4 | from sqlalchemy.orm import Query 5 | 6 | 7 | @dataclass 8 | class Pagination: 9 | page: int 10 | pages: int 11 | items: list 12 | 13 | @property 14 | def has_prev(self) -> bool: 15 | return self.page > 1 16 | 17 | @property 18 | def prev_num(self) -> int | None: 19 | return self.page - 1 if self.page > 1 else None 20 | 21 | @property 22 | def has_next(self) -> bool: 23 | return self.page < self.pages 24 | 25 | @property 26 | def next_num(self) -> int | None: 27 | return self.page + 1 if self.page < self.pages else None 28 | 29 | def iter_pages(self) -> Iterable[int]: 30 | return range(1, self.pages + 1) 31 | 32 | 33 | def paginate_query(query: Query, page: int, per_page: int) -> Pagination: 34 | offset = (page - 1) * per_page 35 | items = query[offset:offset + per_page] 36 | count = query.count() 37 | return Pagination( 38 | page=page, 39 | pages=(count + per_page - 1) // per_page, 40 | items=items 41 | ) 42 | -------------------------------------------------------------------------------- /controlserver/events.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from typing_extensions import override 3 | 4 | 5 | class CTFEvents: 6 | """ 7 | Extend this class to listen to timing-relevant events. Timer.listener.append registers event listeners. 8 | """ 9 | 10 | def on_start_tick(self, tick: int, ts: datetime.datetime) -> None: 11 | pass 12 | 13 | def on_end_tick(self, tick: int, ts: datetime.datetime) -> None: 14 | pass 15 | 16 | def on_start_ctf(self) -> None: 17 | pass 18 | 19 | def on_suspend_ctf(self) -> None: 20 | pass 21 | 22 | def on_end_ctf(self) -> None: 23 | pass 24 | 25 | def on_update_times(self) -> None: 26 | pass 27 | 28 | 29 | class ConsoleCTFEvents(CTFEvents): 30 | """ 31 | Example implementation of the CTFEvents interface 32 | """ 33 | 34 | def __now(self, ts: datetime.datetime) -> str: 35 | return ts.astimezone().strftime('%d.%m.%Y %H:%M:%S') + ' |' 36 | 37 | @override 38 | def on_start_tick(self, tick: int, ts: datetime.datetime) -> None: 39 | print(self.__now(ts), 'Start of tick {}'.format(tick)) 40 | 41 | @override 42 | def on_end_tick(self, tick: int, ts: datetime.datetime) -> None: 43 | print(self.__now(ts), 'End of tick {}'.format(tick)) 44 | 45 | @override 46 | def on_start_ctf(self) -> None: 47 | print(self.__now(datetime.datetime.now()), 'CTF initially started') 48 | 49 | @override 50 | def on_suspend_ctf(self) -> None: 51 | print(self.__now(datetime.datetime.now()), 'CTF suspended') 52 | 53 | @override 54 | def on_end_ctf(self) -> None: 55 | print(self.__now(datetime.datetime.now()), 'CTF is over!') 56 | -------------------------------------------------------------------------------- /controlserver/master_timer.py: -------------------------------------------------------------------------------- 1 | if __name__ == '__main__': 2 | import sys 3 | import os 4 | 5 | sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 6 | 7 | from checker_runner.runner import celery_worker 8 | from controlserver.models import init_database 9 | from controlserver.timer import init_timer, run_master_timer 10 | from saarctf_commons.config import load_default_config 11 | from saarctf_commons.redis import NamedRedisConnection 12 | from saarctf_commons.logging_utils import setup_script_logging 13 | 14 | if __name__ == '__main__': 15 | load_default_config() 16 | setup_script_logging('timer') 17 | NamedRedisConnection.set_clientname('timer', True) 18 | init_database() 19 | init_timer(True) 20 | celery_worker.init() 21 | run_master_timer() 22 | -------------------------------------------------------------------------------- /controlserver/model_admin.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from flask_admin import Admin 3 | from flask_admin.contrib import sqla 4 | 5 | from controlserver.models import Service, Team, db_session 6 | 7 | 8 | class ServiceAdmin(sqla.ModelView): 9 | form_excluded_columns = ['package', 'setup_package'] 10 | column_editable_list = ['name', 'checker_timeout', 'checker_enabled', 'checker_runner', 'checker_route', 'num_payloads', 11 | 'flag_ids', 'flags_per_tick', 'ports'] 12 | column_list = ['id', 'name', 'checker_runner', 13 | 'checker_script_dir', 'checker_script', 'checker_timeout', 'checker_enabled', 'checker_subprocess', 'checker_route', 14 | 'runner_config', 'num_payloads', 'flag_ids', 'flags_per_tick', 'ports'] 15 | create_modal = True 16 | edit_modal = True 17 | column_display_pk = True 18 | pass 19 | 20 | 21 | class TeamAdmin(sqla.ModelView): 22 | form_excluded_columns = ['logo', 'points'] 23 | column_editable_list = ['name', 'affiliation', 'website'] 24 | create_modal = True 25 | edit_modal = True 26 | column_display_pk = True 27 | pass 28 | 29 | 30 | def register_admin(app: Flask) -> None: 31 | admin = Admin(app, name='saarCTF', template_mode='bootstrap3', base_template='admin_layout.html') 32 | admin.add_view(ServiceAdmin(Service, db_session())) 33 | admin.add_view(TeamAdmin(Team, db_session())) 34 | -------------------------------------------------------------------------------- /controlserver/scoring/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarkusBauer/saarctf-gameserver/88bee31ca3bf56492714eac5d0c6f96bc1a92079/controlserver/scoring/__init__.py -------------------------------------------------------------------------------- /controlserver/scoring/algorithms/factory.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from controlserver.models import Service 4 | from controlserver.scoring.algorithms.algorithm import ScoreTickAlgorithm 5 | from controlserver.utils.import_factory import ImportFactory 6 | from saarctf_commons.config import ScoringConfig 7 | 8 | 9 | class ScoreAlgorithmFactory(ImportFactory[ScoreTickAlgorithm]): 10 | base_class = ScoreTickAlgorithm 11 | 12 | @classmethod 13 | def build(cls, config: ScoringConfig, team_ids: list[int], services: list[Service], **kwargs: Any) -> ScoreTickAlgorithm: 14 | return cls.get_class(config.algorithm)(config, team_ids, services, **kwargs) 15 | -------------------------------------------------------------------------------- /controlserver/scoring/scoreboard_process.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | 5 | sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) 6 | 7 | from controlserver.models import init_database 8 | from controlserver.scoring.scoreboard import run_scoreboard_generator 9 | from controlserver.timer import init_slave_timer 10 | from saarctf_commons.config import load_default_config, config 11 | from saarctf_commons.logging_utils import setup_script_logging 12 | from saarctf_commons.redis import NamedRedisConnection 13 | 14 | if __name__ == '__main__': 15 | load_default_config() 16 | config.set_script() 17 | setup_script_logging('scoreboard') 18 | NamedRedisConnection.set_clientname('scoreboard', True) 19 | init_database() 20 | init_slave_timer() 21 | try: 22 | run_scoreboard_generator() 23 | except KeyboardInterrupt: 24 | print('Scoreboard generator terminated') 25 | -------------------------------------------------------------------------------- /controlserver/static/.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | /fonts/ 3 | -------------------------------------------------------------------------------- /controlserver/static/css/.gitignore: -------------------------------------------------------------------------------- 1 | /index.css 2 | -------------------------------------------------------------------------------- /controlserver/static/img/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarkusBauer/saarctf-gameserver/88bee31ca3bf56492714eac5d0c6f96bc1a92079/controlserver/static/img/favicon.png -------------------------------------------------------------------------------- /controlserver/static/img/profile_dummy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarkusBauer/saarctf-gameserver/88bee31ca3bf56492714eac5d0c6f96bc1a92079/controlserver/static/img/profile_dummy.png -------------------------------------------------------------------------------- /controlserver/static/js/general.js: -------------------------------------------------------------------------------- 1 | QueryString = { 2 | parse: function () { 3 | let qs = location.search.substring(1); 4 | if (!qs) 5 | return {}; 6 | let vars = qs.split('&'); 7 | let params = {}; 8 | for (let i = 0; i < vars.length; i++) { 9 | let pair = vars[i].split('='); 10 | if (pair.length >= 2) 11 | params[pair[0]] = decodeURIComponent(pair[1]); 12 | } 13 | return params; 14 | }, 15 | 16 | join: function (params) { 17 | let qs = []; 18 | for (key in params) { 19 | if (params.hasOwnProperty(key) && params[key] !== undefined) 20 | qs.push(key + '=' + encodeURIComponent(params[key])) 21 | } 22 | return '?' + qs.join('&'); 23 | }, 24 | 25 | update: function (changes) { 26 | let params = QueryString.parse(); 27 | for (let key in changes) { 28 | if (changes.hasOwnProperty(key)) 29 | params[key] = changes[key]; 30 | } 31 | location.search = QueryString.join(params); 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /controlserver/static/js/graphs.js: -------------------------------------------------------------------------------- 1 | function MyGraph(element, series, title, options) { 2 | this.labels = []; 3 | this.data = []; 4 | this.datasets = []; 5 | for (let i = 0; i < series.length; i++) { 6 | this.data.push([]); 7 | this.datasets.push({label: series[i], data: this.data[i], cubicInterpolationMode: 'monotone'}); 8 | } 9 | 10 | // default options 11 | options.title = {display: true, text: title}; 12 | options.legend = {display: true, position: 'bottom'}; 13 | if (options.scales === undefined) options.scales = {}; 14 | if (options.scales.xAxes === undefined) 15 | options.scales.xAxes = [{ 16 | type: 'time', 17 | time: { 18 | unit: 'minute', 19 | displayFormats: {minute: 'HH:mm'} 20 | } 21 | }]; 22 | 23 | this.graph = { 24 | type: 'line', 25 | data: {datasets: this.datasets, labels: this.labels}, 26 | options: options 27 | }; 28 | this.graph.data.datasets[0].borderColor = 'rgba(151, 187, 205, 1)'; 29 | this.graph.data.datasets[0].backgroundColor = 'rgba(151, 187, 205, 0.2)'; 30 | this.chart = new Chart(element.getContext('2d'), this.graph); 31 | 32 | this.update = function () { 33 | this.chart.update(); 34 | }; 35 | } 36 | -------------------------------------------------------------------------------- /controlserver/static/js/packages.js: -------------------------------------------------------------------------------- 1 | const app = angular.module('ControlServerApp', []); 2 | 3 | const CTFTimer = { 4 | STOPPED: 1, 5 | SUSPENDED: 2, 6 | RUNNING: 3 7 | }; 8 | 9 | app.controller('PackagesController', function ($scope, $http) { 10 | $scope.FLOWER_URL = FLOWER_URL; 11 | $scope.CHECKER_RESULT_URL = CHECKER_RESULT_URL.replace('123456789', ''); 12 | 13 | $scope.messageList = []; 14 | $scope.updateCheckers = function () { 15 | $http.post('/packages/update', {}).then(function (xhr) { 16 | $scope.messageList.push(xhr.data.join('\n')); 17 | }); 18 | }; 19 | 20 | $scope.updateSingleChecker = function (serviceId) { 21 | if (!serviceId) return; 22 | $http.post('/packages/update', {service: serviceId}).then(function (xhr) { 23 | $scope.messageList.push(xhr.data.join('\n')); 24 | }); 25 | }; 26 | 27 | $scope.pushPackages = function () { 28 | $http.post('/packages/push', {}).then(function (xhr) { 29 | $scope.messageList.push(xhr.data); 30 | }); 31 | }; 32 | 33 | $scope.commands = []; 34 | $scope.runCommands = function (command) { 35 | console.log(command); 36 | $http.post('/packages/run', {command: command}).then(function (xhr) { 37 | if (xhr.data) { 38 | $scope.commands.push({cmd: command, task: xhr.data}); 39 | } 40 | }); 41 | }; 42 | 43 | $scope.testRound = ''; 44 | $scope.testResults = []; 45 | $scope.testScript = function (serviceId, teamId, round) { 46 | if (!serviceId) 47 | return alert('Select service'); 48 | if (!teamId) 49 | return alert('Select team'); 50 | $http.post('/packages/test', {service_id: serviceId, team_id: teamId, round: round}).then(function (xhr) { 51 | if (xhr.data) { 52 | xhr.data.time = Date.now(); 53 | $scope.testResults.push(xhr.data); 54 | } 55 | }); 56 | }; 57 | $scope.testTeam = $('.team-select option[selected]').val(); 58 | }); 59 | 60 | -------------------------------------------------------------------------------- /controlserver/static/js/pagination.js: -------------------------------------------------------------------------------- 1 | $(function () { 2 | $('.filter_checkbox_list').submit(function (e) { 3 | e.preventDefault(); 4 | e.stopPropagation(); 5 | let items = ''; 6 | for (let input of $(this).find('input[type=checkbox]')) { 7 | if (input.checked) { 8 | if (items !== '') items += '|'; 9 | items += input.value; 10 | } 11 | } 12 | let update = {}; 13 | update[$(this).data('param')] = items; 14 | QueryString.update(update); 15 | }); 16 | 17 | $('.filter_options').change(function (e) { 18 | e.preventDefault(); 19 | e.stopPropagation(); 20 | let update = {}; 21 | update[$(this).data('param')] = $(this).val() || undefined; 22 | QueryString.update(update); 23 | }); 24 | 25 | $('.sort-link').click(function (e) { 26 | e.preventDefault(); 27 | e.stopPropagation(); 28 | let key = $(this).data('sort'); 29 | let params = QueryString.parse(); 30 | let order = params.sort === key ? (params.dir === 'desc' ? 'asc' : 'desc') : 'asc'; 31 | QueryString.update({sort: key, dir: order}); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /controlserver/static/less/color.less: -------------------------------------------------------------------------------- 1 | @color0: #0f0; 2 | @color1: #e9ecef; 3 | @color2: #868e96; 4 | @color3: #0f0; 5 | @color4: #007bff; 6 | @color4_2: #025b95; 7 | @color5: #212529; 8 | @color6: #025b95; 9 | 10 | 11 | @brand-success: #28a745; 12 | @brand-info: #009cc4; 13 | @brand-warning: #ffc107; 14 | @brand-danger: #dc3545; 15 | 16 | 17 | 18 | // My lavish boilerplate 19 | // @color0: body background 20 | //@body-bg: @color0; 21 | 22 | // @color1: disabled input & button background, input addon background, nav & tabs & pagination link hover background, jumbotron background 23 | @gray-lighter: @color1; 24 | 25 | // @color2: disabled link color, input placeholder color, dropdown header color, navbar inverse & link color 26 | @gray-light: @color2; 27 | 28 | // @color3: nav tab link hover color 29 | // @gray: @color3; 30 | 31 | // @color4: link color, primary button background, pagination active background, progress bar background, label background, panel heading color 32 | @brand-primary: @color4_2; 33 | 34 | // @color5: text color, legend color, dropdown link color, panel text color, code color 35 | @gray-dark: @color5; 36 | @table-border-color: #e9ecef; 37 | 38 | // @color6: navbar inverse background 39 | @navbar-inverse-bg: @color6; 40 | @navbar-inverse-color: darken(@gray-lighter, 0%); 41 | @navbar-inverse-link-color: darken(@gray-lighter, 0%); 42 | 43 | 44 | @panel-footer-bg: lighten(@color1, 5%); 45 | @panel-default-heading-bg: lighten(@color1, 5%); 46 | 47 | 48 | //@code-color: #bd4147; 49 | @code-color: darken(@brand-primary, 10%); 50 | @code-bg: #f8f9fa; 51 | -------------------------------------------------------------------------------- /controlserver/static/less/index.less: -------------------------------------------------------------------------------- 1 | @import "bootstrap/less/bootstrap.less"; // bootstrap 2 | @import "eonasdan-bootstrap-datetimepicker/src/less/_bootstrap-datetimepicker.less"; // bootstrap plugins 3 | @import "bootstrap/less/theme.less"; // bootstrap 4 | @import "color.less"; 5 | 6 | @import "theme.less"; -------------------------------------------------------------------------------- /controlserver/static/less/theme.less: -------------------------------------------------------------------------------- 1 | @import "bootstrap/less/mixins/gradients.less"; 2 | 3 | .navbar-text.navbar-right { 4 | margin-right: 0px; 5 | } 6 | 7 | #content { 8 | margin-top: 90px; 9 | margin-bottom: 90px; 10 | } 11 | 12 | [ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak { 13 | display: none !important; 14 | } 15 | 16 | .bg-primary-theme { 17 | color: #fff; 18 | .bg-variant(@brand-primary); 19 | #gradient > .vertical(@start-color: lighten(@navbar-inverse-bg, 10%); @end-color: @navbar-inverse-bg); 20 | } 21 | 22 | a.href { 23 | cursor: pointer; 24 | } 25 | 26 | .dl-horizontal.dl-horizontal-wide { 27 | dt { 28 | width: 220px; 29 | } 30 | dd { 31 | margin-left: 240px; 32 | } 33 | } 34 | 35 | 36 | .table.table-middle { 37 | tbody { 38 | td, th { 39 | vertical-align: middle; 40 | } 41 | } 42 | } 43 | 44 | .more-line-height { 45 | line-height: 1.5; 46 | } 47 | -------------------------------------------------------------------------------- /controlserver/templates/404.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}Not Found{% endblock %} 3 | 4 | {% block content %} 5 |

404 Not Found

6 | {% if message %} 7 |

{{ message }}

8 | {% else %} 9 |

This page has not been found

10 | {% endif %} 11 | {% endblock %} 12 | 13 | {% block footer %} 14 | {% endblock %} 15 | -------------------------------------------------------------------------------- /controlserver/templates/dashboard/panel-components.html: -------------------------------------------------------------------------------- 1 | {% raw %} 2 |
3 |
4 | 6 | Components 7 |
8 |
9 |

10 | Redis connections: 11 |

12 |

13 | {{ count }}× {{ name || '(unnamed)' }} 14 |

15 |
16 |
17 | Connected 18 | {{ client.name || '(unnamed)' }} 19 | ({{ client.addr }}) 20 | (sub) 21 | (pub) 22 | ({{ client.combine_count }}×) 23 |
24 |
25 | Disconnected 26 | {{ client.name || '(unnamed)' }} 27 | ({{ client.addr }}) 28 | (sub) 29 |
30 |
31 |
32 |
33 | 34 | 39 | {% endraw %} 40 | -------------------------------------------------------------------------------- /controlserver/templates/dashboard/panel-flower.html: -------------------------------------------------------------------------------- 1 | {% raw %} 2 |
3 |
4 | 6 | Celery Workers 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 |
WorkerStatus#ProcessesTasks
(active / total)
Load Avg.
{{ worker.hostname }} 22 | online 23 | offline 24 | {{ concurrency[worker.hostname] || '?' }}{{ worker.active }} / {{ worker['task-started'] }}{{ worker.loadavg[0].toFixed(1) }}, {{ worker.loadavg[1].toFixed(1) }}, {{ worker.loadavg[2].toFixed(1) }}
{{ online }} / {{ workers.length }} workers online.
36 |
37 | {% endraw %} 38 | -------------------------------------------------------------------------------- /controlserver/templates/dashboard/panel-logs.html: -------------------------------------------------------------------------------- 1 | {% raw %} 2 | 29 | {% endraw %} 30 | -------------------------------------------------------------------------------- /controlserver/templates/log_messages_view.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}Log #{{ log_message.id }}{% endblock %} 3 | 4 | {% block content %} 5 |

6 | ⇚ Previous 7 | Next ⇛ 8 |

9 | 10 |
11 |
ID
12 |
{{ log_message.id }}
13 |
Created
14 |
{{ log_message.created.strftime('%d.%m.%Y %H:%M:%S') }}
15 |
Component
16 |
{{ log_message.component }}
17 |
Level
18 |
19 | {% if log_message.level == LogMessage.ERROR %} 20 | Error 21 | {% elif log_message.level == LogMessage.WARNING %} 22 | Warning 23 | {% elif log_message.level == LogMessage.IMPORTANT %} 24 | Important 25 | {% elif log_message.level == LogMessage.INFO %} 26 | Info 27 | {% elif log_message.level == LogMessage.DEBUG %} 28 | Debug 29 | {% endif %} 30 | ({{ log_message.level }}) 31 |
32 |
Title
33 |
{{ log_message.title or '-'|safe }}
34 | 35 |
36 | {% if log_message.text %} 37 |
{{ log_message.text }}
38 | {% else %} 39 |

(no text)

40 | {% endif %} 41 | {% endblock %} 42 | -------------------------------------------------------------------------------- /controlserver/utils/import_factory.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | import inspect 3 | import sys 4 | from abc import ABC 5 | from functools import lru_cache 6 | from pathlib import Path 7 | from typing import Type, ParamSpec, TypeVar, Generic 8 | 9 | P = ParamSpec('P') 10 | T = TypeVar('T') 11 | 12 | 13 | class ImportFactory(ABC, Generic[T]): 14 | base_class: Type[T] 15 | 16 | @classmethod 17 | @lru_cache() 18 | def get_class(cls, requested_class: str) -> Type[T]: 19 | cls.fix_import_path() 20 | 21 | module_name, class_name = requested_class.split(':', 1) 22 | package = cls.__module__[:cls.__module__.rindex('.')] 23 | 24 | module = importlib.import_module(package + '.' + module_name, package=package) 25 | clstype = getattr(module, class_name) 26 | if not inspect.isclass(clstype) or not issubclass(clstype, cls.base_class): 27 | raise Exception(f'Class {class_name} in {package} is missing or not a {cls.base_class.__name__}') 28 | return clstype 29 | 30 | @classmethod 31 | @lru_cache() 32 | def fix_import_path(cls) -> None: 33 | path = str(Path(__file__).absolute().parent.parent.parent) 34 | if path not in sys.path: 35 | sys.path = [path] + sys.path 36 | -------------------------------------------------------------------------------- /controlserver/wsgi.py: -------------------------------------------------------------------------------- 1 | from controlserver.app import create_app 2 | 3 | app = create_app() 4 | -------------------------------------------------------------------------------- /flag-submission-server/.dockerignore: -------------------------------------------------------------------------------- 1 | cmake-build-* 2 | build 3 | .idea 4 | -------------------------------------------------------------------------------- /flag-submission-server/.gitignore: -------------------------------------------------------------------------------- 1 | /cmake-build-debug 2 | bucket.txt 3 | /build 4 | /build-sanitizer/ 5 | /cmake-build-release/ 6 | -------------------------------------------------------------------------------- /flag-submission-server/.idea/.gitignore: -------------------------------------------------------------------------------- 1 | /workspace.xml 2 | /misc.xml 3 | /dbnavigator.xml 4 | -------------------------------------------------------------------------------- /flag-submission-server/.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /flag-submission-server/.idea/flag-submission-server.iml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /flag-submission-server/.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /flag-submission-server/.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /flag-submission-server/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:trixie AS base 2 | 3 | # optional: use a local apt-cacher-ng to speed things up 4 | # RUN IP=172.17.0.1 && echo "Acquire::http { Proxy \"http://$IP:3142\"; }" > /etc/apt/apt.conf.d/01proxy 5 | 6 | # install runtime libraries + headers 7 | RUN export DEBIAN_FRONTEND=noninteractive && \ 8 | apt-get update && \ 9 | apt-get install -y --no-install-recommends libev-dev libhiredis-dev libpq-dev libssl-dev && \ 10 | apt-get clean 11 | 12 | 13 | 14 | FROM base AS build 15 | 16 | # install a C++ compiler and toolchain 17 | RUN export DEBIAN_FRONTEND=noninteractive && \ 18 | apt-get update && \ 19 | apt-get install -y --no-install-recommends g++ cmake make git ca-certificates postgresql-server-dev-all && \ 20 | apt-get clean 21 | 22 | RUN mkdir /src && mkdir /build 23 | 24 | ADD benchmark /src/benchmark 25 | ADD cmake /src/cmake 26 | ADD src /src/src 27 | ADD tests /src/tests 28 | ADD CMakeLists.txt /src/ 29 | 30 | RUN cd /build && \ 31 | cmake -DCMAKE_BUILD_TYPE=Release -DPostgreSQL_ADDITIONAL_VERSIONS=17 /src && \ 32 | make -j4 flag-submission-server 33 | 34 | 35 | 36 | FROM base AS runtime 37 | 38 | COPY --from=build /build/flag-submission-server /flag-submission-server 39 | 40 | WORKDIR / 41 | USER nobody 42 | EXPOSE 31337 43 | ENV SAARCTF_CONFIG=/config.yaml 44 | 45 | CMD ["/flag-submission-server", "31337", "8"] 46 | -------------------------------------------------------------------------------- /flag-submission-server/README.md: -------------------------------------------------------------------------------- 1 | 2 | Building 3 | -------- 4 | You need `libev`, `openssl`, `libhiredis` and `libpq`. On Ubuntu: `apt install libev-dev libssl-dev libpq-dev postgresql-server-dev-all libhiredis-dev cmake` 5 | 6 | Build using CMake: 7 | ``` 8 | mkdir build 9 | cd build 10 | cmake -DCMAKE_BUILD_TYPE=Release .. 11 | make 12 | ``` 13 | 14 | 15 | Usage 16 | ----- 17 | ``` 18 | ./flag-submission-server 19 | ``` 20 | Default port: 31337, default threads: 1. 21 | 22 | 23 | Flag format 24 | ----------- 25 | `SAAR{32-websafe-base64-bytes}` (example: `SAAR{vQA2AAYAAACXlPecBGL77CAqZOuU4BTa}`, regex `SAAR\{[A-Za-z0-9-_]{32}\}`) 26 | 27 | Binary data consists of (all numbers are little-endian): 28 | 29 | - 2 bytes tick (this flag was stored) 30 | - 2 bytes team id 31 | - 2 bytes service id 32 | - 2 payload bytes 33 | - 16 bytes SHA256-HMAC (over the other parts of the flag) 34 | 35 | Configure in [`flagchecker.h`](src/flagchecker.h) and [`flagchecker.cpp`](src/flagchecker.cpp). 36 | 37 | 38 | Configuration 39 | ------------- 40 | In [`config.yaml`](../config.sample.yaml): set everything you need (database, redis, secret key, ...). This should be enough for most purposes. 41 | 42 | In [`flagcache.cpp`](src/flagcache.cpp): Set flag rate and number of valid flags per service/team/round. 43 | 44 | In [`flagchecker.h`](src/flagchecker.h): Enable/disable what to check for. 45 | 46 | In [`database.cpp`](src/database.cpp): Enable or disable asynchronous commits. 47 | 48 | Run `make` afterwards. 49 | 50 | 51 | Database 52 | -------- 53 | This server needs the database layout from the `saarctf` gameserver. It does not initialize the database itself. 54 | -------------------------------------------------------------------------------- /flag-submission-server/cmake/FindLibHiredis.cmake: -------------------------------------------------------------------------------- 1 | # - Try to find the Lib library 2 | # Once done this will define 3 | # 4 | # LIBHIREDIS_FOUND - System has Hiredis 5 | # LIBHIREDIS_INCLUDE_DIR - The Hiredis include directory 6 | # LIBHIREDIS_LIBRARIES - The libraries needed to use Hiredis 7 | # LIBHIREDIS_DEFINITIONS - Compiler switches required for using Hiredis 8 | 9 | 10 | # use pkg-config to get the directories and then use these values 11 | # in the FIND_PATH() and FIND_LIBRARY() calls 12 | #FIND_PACKAGE(PkgConfig) 13 | #PKG_SEARCH_MODULE(PC_LIBHIREDIS libhiredis) 14 | 15 | SET(LIBHIREDIS_DEFINITIONS ${PC_LIBHIREDIS_CFLAGS_OTHER}) 16 | 17 | FIND_PATH(LIBHIREDIS_INCLUDE_DIR hiredis/hiredis.h 18 | HINTS 19 | ${PC_LIBHIREDIS_INCLUDEDIR} 20 | ${PC_LIBHIREDIS_INCLUDE_DIRS} 21 | ) 22 | 23 | FIND_LIBRARY(LIBHIREDIS_LIBRARIES NAMES hiredis 24 | HINTS 25 | ${PC_LIBHIREDIS_LIBDIR} 26 | ${PC_LIBHIREDIS_LIBRARY_DIRS} 27 | ) 28 | 29 | INCLUDE(FindPackageHandleStandardArgs) 30 | FIND_PACKAGE_HANDLE_STANDARD_ARGS(LibHiredis DEFAULT_MSG LIBHIREDIS_LIBRARIES LIBHIREDIS_INCLUDE_DIR) 31 | 32 | #MARK_AS_ADVANCED(LIBHIREDIS_INCLUDE_DIR LIBHIREDIS_LIBRARIES) -------------------------------------------------------------------------------- /flag-submission-server/cmake/FindLibev.cmake: -------------------------------------------------------------------------------- 1 | # Try to find libev 2 | # Once done, this will define 3 | # 4 | # LIBEV_FOUND - system has libev 5 | # LIBEV_INCLUDE_DIRS - libev include directories 6 | # LIBEV_LIBRARIES - libraries needed to use libev 7 | 8 | if(LIBEV_INCLUDE_DIRS AND LIBEV_LIBRARIES) 9 | set(LIBEV_FIND_QUIETLY TRUE) 10 | else() 11 | find_path( 12 | LIBEV_INCLUDE_DIR 13 | NAMES ev.h 14 | HINTS ${LIBEV_ROOT_DIR} 15 | PATH_SUFFIXES include) 16 | 17 | find_library( 18 | LIBEV_LIBRARY 19 | NAME ev 20 | HINTS ${LIBEV_ROOT_DIR} 21 | PATH_SUFFIXES ${CMAKE_INSTALL_LIBDIR}) 22 | 23 | set(LIBEV_INCLUDE_DIRS ${LIBEV_INCLUDE_DIR}) 24 | set(LIBEV_LIBRARIES ${LIBEV_LIBRARY}) 25 | 26 | include(FindPackageHandleStandardArgs) 27 | find_package_handle_standard_args( 28 | Libev DEFAULT_MSG LIBEV_LIBRARY LIBEV_INCLUDE_DIR) 29 | 30 | mark_as_advanced(LIBEV_LIBRARY LIBEV_INCLUDE_DIR) 31 | endif() -------------------------------------------------------------------------------- /flag-submission-server/scripts/generate_bucket.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from generate_flag import generate_flag, config, load_default_config 4 | 5 | """ 6 | USAGE: python3 generate_bucket.py 7 | Generates new and valid flags, and saves them to ./bucket.txt 8 | """ 9 | 10 | if __name__ == "__main__": 11 | load_default_config() 12 | config.set_script() 13 | BUCKET = int(sys.argv[1]) 14 | 15 | bucket = "".join( 16 | [generate_flag(i % 100 + 5, ((3 * i) % 8) + 1) + "\n" for i in range(BUCKET)] 17 | ) 18 | 19 | with open("bucket.txt", "w") as f: 20 | f.write(bucket) 21 | 22 | print(f"Bucket with {BUCKET} flags generated") 23 | -------------------------------------------------------------------------------- /flag-submission-server/scripts/generate_flag.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import hashlib 3 | import hmac 4 | import os 5 | import random 6 | import struct 7 | import sys 8 | 9 | sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) 10 | 11 | from saarctf_commons.config import config, load_default_config 12 | 13 | """ 14 | Generates valid flags 15 | 16 | // Binary flag format (after b64 decode): 24 bytes 17 | // = 32 bytes base64 18 | // = 38 bytes including SAAR{} 19 | // Truncated format: 24bytes => 32chars => 38chars total 20 | struct __attribute__((__packed__)) FlagFormat { 21 | uint16_t tick; 22 | uint16_t team_id; 23 | uint16_t service_id; 24 | uint16_t payload; 25 | char mac[16]; // out of 32 26 | }; 27 | """ 28 | 29 | 30 | def generate_flag(team: int, service: int, game_tick: int | None = None) -> str: 31 | assert team < 2 ** 16 32 | assert service < 2 ** 16 33 | 34 | if not game_tick: 35 | game_tick = 1 36 | assert game_tick < 2 ** 16 37 | flag = struct.pack(" 1 else i, 51 | int(sys.argv[2]) if len(sys.argv) > 2 else 1, 52 | ) 53 | ) 54 | -------------------------------------------------------------------------------- /flag-submission-server/scripts/submit_new_flags.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import sys 3 | from pwn import remote 4 | 5 | from generate_flag import generate_flag, config, load_default_config 6 | 7 | """ 8 | USAGE: python3 submit_new_flags.py 9 | Submits new and valid flags to localhost:31337 10 | """ 11 | 12 | load_default_config() 13 | config.set_script() 14 | 15 | HOST = "localhost" 16 | PORT = 31337 17 | COUNT = int(sys.argv[1]) 18 | 19 | conn = remote(HOST, PORT) 20 | print("Connected.") 21 | 22 | print(f"Sending {COUNT} flags ...") 23 | success = 0 24 | for i in range(COUNT): 25 | conn.write((generate_flag(2, 1, 100) + "\n").encode()) 26 | tmp = conn.readuntil(b"\n") 27 | if tmp != b"[OK]\n": 28 | print(tmp.strip()) 29 | else: 30 | success += 1 31 | 32 | # half close 33 | conn.shutdown() 34 | conn.readall() 35 | # full close 36 | conn.close() 37 | 38 | print(f"Done. {success}") 39 | if success != COUNT: 40 | sys.exit(1) 41 | -------------------------------------------------------------------------------- /flag-submission-server/src/config.h: -------------------------------------------------------------------------------- 1 | #ifndef FLAG_SUBMISSION_SERVER_CONFIG_H 2 | #define FLAG_SUBMISSION_SERVER_CONFIG_H 3 | 4 | #include 5 | #include 6 | 7 | class Config { 8 | public: 9 | static void load(); 10 | 11 | static void load(const std::string& filename); 12 | 13 | static const char *getPostgresConnectionString(); 14 | 15 | static std::string getRedisHost(); 16 | 17 | static int getRedisPort(); 18 | 19 | static int getRedisDB(); 20 | 21 | static std::string getRedisPassword(); 22 | 23 | static unsigned char hmac_secret_key[32]; 24 | 25 | static int flagRoundsValid; 26 | 27 | static int nopTeamId; 28 | 29 | static uint16_t getTeamIdFromIp(uint8_t ip0, uint8_t ip1, uint8_t ip2, uint8_t ip3); 30 | }; 31 | 32 | #endif //FLAG_SUBMISSION_SERVER_CONFIG_H 33 | -------------------------------------------------------------------------------- /flag-submission-server/src/database.h: -------------------------------------------------------------------------------- 1 | #ifndef LIBEV_SERVER_DATABASE_H 2 | #define LIBEV_SERVER_DATABASE_H 3 | 4 | #include 5 | 6 | class FlagFormat; 7 | 8 | /** 9 | * Submits a flag to the database 10 | * @param team submitting team 11 | * @param flag binary part of the flag 12 | * @return 1 if the flag was new and accepted, 0 if the flag was already present, negative values for error 13 | */ 14 | int submit_flag(uint16_t team, FlagFormat& flag); 15 | 16 | int getMaxTeamId(); 17 | int getMaxServiceId(); 18 | 19 | #endif //LIBEV_SERVER_DATABASE_H 20 | -------------------------------------------------------------------------------- /flag-submission-server/src/flagcache.h: -------------------------------------------------------------------------------- 1 | #ifndef LIBEV_SERVER_FLAGCACHE_H 2 | #define LIBEV_SERVER_FLAGCACHE_H 3 | 4 | #include 5 | #include 6 | 7 | 8 | class FlagCache { 9 | private: 10 | uint32_t team_count; 11 | uint32_t service_count; 12 | uint32_t round_buckets; 13 | uint32_t payload_buckets; 14 | std::atomic_uint *cache = nullptr; 15 | 16 | std::atomic_long cache_hits; 17 | std::atomic_long cache_misses; 18 | std::atomic_long cache_fails; 19 | 20 | public: 21 | FlagCache(); 22 | 23 | FlagCache(uint32_t team_count, uint32_t service_count); 24 | 25 | ~FlagCache(); 26 | 27 | void resize(uint32_t team_count, uint32_t service_count); 28 | 29 | void printStats(); 30 | 31 | /** 32 | * 33 | * @param submitting_team 34 | * @param team_id 35 | * @param expires 36 | * @return true is possibly new, false if it was already present there (definitely not new) 37 | */ 38 | bool checkFlag(uint16_t submitting_team, uint16_t team_id, uint16_t service_id, uint16_t round, uint16_t payload); 39 | 40 | /** 41 | * call this if an already existing entry was not found in the cache before 42 | */ 43 | void cacheFailed(); 44 | 45 | /** 46 | * @return number of flags that were cached 47 | */ 48 | long getCacheHits() { 49 | return cache_hits; 50 | } 51 | 52 | /** 53 | * @return number of flags that were not in cache 54 | */ 55 | long getCacheMisses() { 56 | return cache_misses; 57 | } 58 | 59 | /** 60 | * @return number of flags that were not in cache, but have been already submitted 61 | */ 62 | long getCacheFails() { 63 | return cache_fails; 64 | } 65 | }; 66 | 67 | #endif //LIBEV_SERVER_FLAGCACHE_H 68 | -------------------------------------------------------------------------------- /flag-submission-server/src/flagchecker.h: -------------------------------------------------------------------------------- 1 | #ifndef LIBEV_SERVER_FLAGCHECKER_H 2 | #define LIBEV_SERVER_FLAGCHECKER_H 3 | 4 | 5 | #include 6 | #include 7 | #include "flagcache.h" 8 | 9 | 10 | // Enable/disable checking steps (for benchmark) 11 | #define CHECK_MAC 12 | #define CHECK_EXPIRED 13 | #define CHECK_CACHE 14 | #define CHECK_STATE 15 | 16 | 17 | // FORMAT: SAAR{QUFBQUFBQUFCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkFB} 18 | 19 | // Binary flag format (after b64 decode): 39 bytes 20 | // = 56 bytes base64 21 | // = 62 bytes including SAAR{} 22 | // Truncated format: 24bytes => 32chars => 38chars total 23 | struct __attribute__((__packed__)) FlagFormat { 24 | uint16_t round; 25 | uint16_t team_id; 26 | uint16_t service_id; 27 | uint16_t payload; 28 | char mac[16]; // out of 32 29 | }; 30 | 31 | #define FLAG_LENGTH_B64 32 32 | #define FLAG_LENGTH_FULL 38 33 | 34 | 35 | /* 36 | * If you want to change the flag format: 37 | * - Change the struct above 38 | * - Ensure mac is the last field 39 | * - Change the length constants 40 | */ 41 | 42 | 43 | // valid team ids: [1 .. max_team_id] 44 | extern uint32_t max_team_id; 45 | // valid service ids: [1 .. max_service_id] 46 | extern uint32_t max_service_id; 47 | 48 | 49 | void initModelSizes(uint32_t _max_team_id, uint32_t _max_service_id); 50 | 51 | const char *progress_flag(const char *flag, int len, struct sockaddr_in *addr, uint16_t *team_id_cache); 52 | 53 | bool verify_hmac(void *data_start, void *data_end, const char *hmac); 54 | 55 | void create_hmac(void *data_start, void *data_end, char *hmac_out); 56 | 57 | void printFlagStatsForRound(int round); 58 | 59 | void printCacheStats(); 60 | 61 | extern FlagCache flag_cache; 62 | 63 | #endif //LIBEV_SERVER_FLAGCHECKER_H 64 | -------------------------------------------------------------------------------- /flag-submission-server/src/libraries/base64.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Base64 encoding/decoding (RFC1341) 3 | * Copyright (c) 2005, Jouni Malinen 4 | * 5 | * This software may be distributed under the terms of the BSD license. 6 | * See README for more details. 7 | */ 8 | 9 | #ifndef BASE64_H 10 | #define BASE64_H 11 | 12 | #ifdef __cplusplus 13 | extern "C" { 14 | #endif 15 | 16 | #include 17 | 18 | void base64_encode(const unsigned char *src, size_t len, char *out); 19 | 20 | size_t base64_decode(const unsigned char *src, size_t len, unsigned char *out); 21 | 22 | #ifdef __cplusplus 23 | } 24 | #endif 25 | 26 | #endif /* BASE64_H */ 27 | -------------------------------------------------------------------------------- /flag-submission-server/src/periodic.cpp: -------------------------------------------------------------------------------- 1 | #include "periodic.h" 2 | #include "database.h" 3 | #include "flagchecker.h" 4 | #include 5 | #include 6 | 7 | void PeriodicMaintenance::checkDatabase(ev::timer &w, int revents) { 8 | uint32_t current_max_teams = std::max(max_team_id, (uint32_t) getMaxTeamId() + 1); 9 | uint32_t current_max_services = std::max(max_service_id, (uint32_t) getMaxServiceId()); 10 | if (current_max_teams > max_team_id || current_max_services > max_service_id) { 11 | std::cout << "[Teams] Number of teams/services changed" << std::endl; 12 | initModelSizes(current_max_teams, current_max_services); 13 | } 14 | } 15 | 16 | void PeriodicMaintenance::connect(struct ev_loop *loop) { 17 | timer.set(); 18 | timer.set(60.0, 60.0); 19 | timer.set(loop); 20 | timer.start(); 21 | } 22 | -------------------------------------------------------------------------------- /flag-submission-server/src/periodic.h: -------------------------------------------------------------------------------- 1 | #ifndef FLAG_SUBMISSION_SERVER_PERIODIC_H 2 | #define FLAG_SUBMISSION_SERVER_PERIODIC_H 3 | 4 | #include 5 | 6 | class PeriodicMaintenance { 7 | ev::timer timer; 8 | 9 | static void checkDatabase(ev::timer &w, int revents); 10 | 11 | public: 12 | void connect(struct ev_loop *loop); 13 | }; 14 | 15 | #endif //FLAG_SUBMISSION_SERVER_PERIODIC_H 16 | -------------------------------------------------------------------------------- /flag-submission-server/src/redis.h: -------------------------------------------------------------------------------- 1 | #ifndef FLAG_SUBMISSION_SERVER_REDIS_H 2 | #define FLAG_SUBMISSION_SERVER_REDIS_H 3 | 4 | 5 | const int STOPPED = 1; 6 | const int SUSPENDED = 2; 7 | const int RUNNING = 3; 8 | 9 | 10 | class Redis { 11 | public: 12 | static volatile int current_round; 13 | static volatile int state; 14 | 15 | static void connect(struct ev_loop *loop); 16 | }; 17 | 18 | 19 | #endif //FLAG_SUBMISSION_SERVER_REDIS_H 20 | -------------------------------------------------------------------------------- /flag-submission-server/src/statistics.h: -------------------------------------------------------------------------------- 1 | #ifndef FLAG_SUBMISSION_SERVER_STATISTICS_H 2 | #define FLAG_SUBMISSION_SERVER_STATISTICS_H 3 | 4 | // echo -e 'statistics connections\nstatistics flags\nstatistics cache' | socat - tcp:localhost:31337 5 | 6 | #include 7 | 8 | # define MAX_TEAMS 2048 9 | 10 | namespace statistics { 11 | 12 | enum FlagState { 13 | New = 0, 14 | Old = 1, 15 | Expired = 2, 16 | Invalid = 3, 17 | Nop = 4, 18 | Own = 5 19 | }; 20 | 21 | void initStatisticSize(int max_teams); 22 | 23 | void countFlag(uint16_t submittingTeam, FlagState state); 24 | 25 | void countConnection(); 26 | 27 | const char *getConnectionFDReport(int current_connection_count); 28 | 29 | std::vector getFlagReport(); 30 | 31 | const char *getCacheReport(); 32 | 33 | } 34 | 35 | #endif //FLAG_SUBMISSION_SERVER_STATISTICS_H 36 | -------------------------------------------------------------------------------- /flag-submission-server/src/windows_fixes/fixes.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | #include 4 | #include 5 | #include "hook.hpp" 6 | 7 | real_function(setsockopt, int(int, int, int, const void*, socklen_t)); 8 | 9 | int hook_setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen) { 10 | if (optname == TCP_NODELAY) 11 | return 0; 12 | return real(setsockopt)(sockfd, level, optname, optval, optlen); 13 | } 14 | install_hook(setsockopt, hook_setsockopt); 15 | -------------------------------------------------------------------------------- /flag-submission-server/src/workerpool.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "workerpool.h" 3 | 4 | 5 | void Worker::mainloop() { 6 | break_handler.set(this); 7 | break_handler.start(); 8 | invoke_handler.set(this); 9 | invoke_handler.start(); 10 | 11 | loop.run(0); 12 | if (!terminating) { 13 | std::cerr << "Worker died" << std::endl; 14 | } 15 | } 16 | 17 | void Worker::terminate_cb(ev::async &sig, int revents) { 18 | terminating = true; 19 | loop.break_loop(); 20 | } 21 | 22 | Worker::Worker() : break_handler(loop), invoke_handler(loop), terminating(false) { 23 | // start thread after everything is initialized 24 | thread = std::thread(&Worker::mainloop, this); 25 | } 26 | 27 | Worker::~Worker() { 28 | // Worker can only be free if worker thread is not running anymore 29 | break_handler.send(); 30 | thread.join(); 31 | } 32 | 33 | void Worker::invoke_cb(ev::async &sig, int revents) { 34 | // invoke has been called from somewhere, process invoke queue 35 | std::lock_guard lockGuard(this->invoke_lock); 36 | while (!invoke_queue.empty()) { 37 | invoke_queue.front()(); 38 | invoke_queue.pop(); 39 | } 40 | } 41 | 42 | 43 | WorkerPool::WorkerPool(int threads) { 44 | for (int i = 0; i < threads; i++) { 45 | workers.push_back(std::make_shared()); 46 | } 47 | } 48 | 49 | WorkerPool::~WorkerPool() { 50 | for (auto &worker: workers) { 51 | worker->break_handler.send(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /flag-submission-server/src/workerpool.h: -------------------------------------------------------------------------------- 1 | #ifndef LIBEV_SERVER_WORKERPOOL_H 2 | #define LIBEV_SERVER_WORKERPOOL_H 3 | 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | 15 | /** 16 | * A thread running an libev eventloop. 17 | * Use invoke() to push functions to the eventloop (that can register additional handlers) 18 | */ 19 | class Worker { 20 | friend class WorkerPool; 21 | public: 22 | ev::dynamic_loop loop; 23 | std::thread thread; 24 | // this handler is triggered to terminate the thread 25 | ev::async break_handler; 26 | 27 | Worker(); 28 | ~Worker(); 29 | 30 | protected: 31 | ev::async invoke_handler; 32 | std::queue> invoke_queue; 33 | std::mutex invoke_lock; 34 | std::atomic_bool terminating{}; 35 | 36 | void mainloop(); 37 | void terminate_cb(ev::async& sig, int revents); 38 | void invoke_cb(ev::async&sig, int revents); 39 | 40 | public: 41 | template 42 | void invoke(K* object){ 43 | std::lock_guard lockGuard(this->invoke_lock); 44 | invoke_queue.push(std::bind(method, object, this)); 45 | invoke_handler.send(); 46 | }; 47 | }; 48 | 49 | 50 | 51 | /** 52 | * A collection of workers. getWorker() gives a random worker back. 53 | * Freeing the pool stops all contained workers. 54 | */ 55 | class WorkerPool{ 56 | std::vector> workers; 57 | int next_worker = 0; 58 | 59 | public: 60 | explicit WorkerPool(int threads); 61 | ~WorkerPool(); 62 | 63 | inline Worker& getWorker(){ 64 | if (next_worker >= workers.size()) next_worker = 0; 65 | return *workers[next_worker++]; 66 | } 67 | }; 68 | 69 | 70 | #endif //LIBEV_SERVER_WORKERPOOL_H 71 | -------------------------------------------------------------------------------- /flag-submission-server/tests/testconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "databases": { 3 | "postgres": { 4 | "__comment": "Leave server empty to use local socket", 5 | "server": "", 6 | "port": 5432, 7 | "username": "", 8 | "password": "", 9 | "database": "saarctf_2" 10 | }, 11 | "redis": { 12 | "host": "localhost", 13 | "port": 6379, 14 | "db": 3 15 | }, 16 | "rabbitmq": { 17 | "host": "localhost", 18 | "port": 5672, 19 | "vhost": "saarctf", 20 | "username": "saarctf", 21 | "password": "123456789" 22 | } 23 | }, 24 | "secret_flags": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", 25 | "scoring": { 26 | "flags_rounds_valid": 10, 27 | "nop_team_id": 1 28 | }, 29 | "network": { 30 | "game": "127.0.0.0/16", 31 | "__ip_syntax": ["number", "or list", ["a", "b", "c"], "= ((team_id / a) mod b) + c"], 32 | "vulnbox_ip": [127, [200, 256, 0], [1, 200, 0], 2], 33 | "gateway_ip": [127, [200, 256, 0], [1, 200, 0], 1], 34 | "__range_syntax": ["number", "or list", ["a", "b", "c"], "= ((team_id / a) mod b) + c", "/range"], 35 | "team_range": [127, [200, 256, 0], [1, 200, 0], 0, 24], 36 | "vpn_host": "10.13.0.1", 37 | "vpn_peer_ips": [127, [200, 256, 52], [1, 200, 0], 1], 38 | "gameserver_ip": "10.13.0.2" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /flag-submission-server/tests/testconfig2.json: -------------------------------------------------------------------------------- 1 | { 2 | "secret_flags": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", 3 | "scoring": { 4 | "flags_rounds_valid": 20, 5 | "nop_team_id": 2, 6 | "off_factor": 1.0, 7 | "def_factor": 1.0, 8 | "sla_factor": 1.0 9 | }, 10 | "network": { 11 | "team_range": [127, [200, 256, 0], [1, 200, 0], 0, 24], 12 | "vpn_peer_ips": [127, [200, 256, 52], [1, 200, 0], 1] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /flag-submission-server/tests/testconfig3.json: -------------------------------------------------------------------------------- 1 | { 2 | "secret_flags": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", 3 | "scoring": { 4 | "flags_rounds_valid": 20, 5 | "nop_team_id": 2, 6 | "off_factor": 1.0, 7 | "def_factor": 1.0, 8 | "sla_factor": 1.0 9 | }, 10 | 11 | "flags_rounds_valid": 5, 12 | "nop_team_id": 3, 13 | 14 | "network": { 15 | "team_range": [127, [200, 256, 0], [1, 200, 0], 0, 24], 16 | "vpn_peer_ips": [127, [200, 256, 52], [1, 200, 0], 1] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /migrations/alembic.ini: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | script_location = migrations 5 | # template used to generate migration files 6 | # file_template = %%(rev)s_%%(slug)s 7 | 8 | # set to 'true' to run the environment during 9 | # the 'revision' command, regardless of autogenerate 10 | # revision_environment = false 11 | 12 | 13 | # Logging configuration 14 | [loggers] 15 | keys = root,sqlalchemy,alembic 16 | 17 | [handlers] 18 | keys = console 19 | 20 | [formatters] 21 | keys = generic 22 | 23 | [logger_root] 24 | level = WARN 25 | handlers = console 26 | qualname = 27 | 28 | [logger_sqlalchemy] 29 | level = WARN 30 | handlers = 31 | qualname = sqlalchemy.engine 32 | 33 | [logger_alembic] 34 | level = INFO 35 | handlers = 36 | qualname = alembic 37 | 38 | [handler_console] 39 | class = StreamHandler 40 | args = (sys.stderr,) 41 | level = NOTSET 42 | formatter = generic 43 | compare_type = True 44 | 45 | [formatter_generic] 46 | format = %(levelname)-5.5s [%(name)s] %(message)s 47 | datefmt = %H:%M:%S 48 | -------------------------------------------------------------------------------- /migrations/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | ${imports if imports else ""} 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = ${repr(up_revision)} 14 | down_revision = ${repr(down_revision)} 15 | branch_labels = ${repr(branch_labels)} 16 | depends_on = ${repr(depends_on)} 17 | 18 | 19 | def upgrade(): 20 | ${upgrades if upgrades else "pass"} 21 | 22 | 23 | def downgrade(): 24 | ${downgrades if downgrades else "pass"} 25 | -------------------------------------------------------------------------------- /migrations/versions/057792e3ba77_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 057792e3ba77 4 | Revises: bead29c0348f 5 | 6 | """ 7 | from alembic import op 8 | import sqlalchemy as sa 9 | 10 | 11 | # revision identifiers, used by Alembic. 12 | revision = '057792e3ba77' 13 | down_revision = 'bead29c0348f' 14 | branch_labels = None 15 | depends_on = None 16 | 17 | 18 | def upgrade(): 19 | # ### commands auto generated by Alembic - please adjust! ### 20 | op.add_column('checker_results', sa.Column('finished', sa.TIMESTAMP(), server_default=sa.text('NULL'), nullable=True)) 21 | # ### end Alembic commands ### 22 | 23 | 24 | def downgrade(): 25 | # ### commands auto generated by Alembic - please adjust! ### 26 | op.drop_column('checker_results', 'finished') 27 | # ### end Alembic commands ### 28 | -------------------------------------------------------------------------------- /migrations/versions/0e400cc111e2_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 0e400cc111e2 4 | Revises: b216b27a5b41 5 | 6 | """ 7 | from alembic import op 8 | import sqlalchemy as sa 9 | 10 | 11 | # revision identifiers, used by Alembic. 12 | revision = '0e400cc111e2' 13 | down_revision = 'b216b27a5b41' 14 | branch_labels = None 15 | depends_on = None 16 | 17 | 18 | def upgrade(): 19 | # ### commands auto generated by Alembic - please adjust! ### 20 | op.add_column('services', sa.Column('checker_subprocess', sa.Boolean(), server_default=sa.text('FALSE'), nullable=False)) 21 | # ### end Alembic commands ### 22 | 23 | 24 | def downgrade(): 25 | # ### commands auto generated by Alembic - please adjust! ### 26 | op.drop_column('services', 'checker_subprocess') 27 | # ### end Alembic commands ### 28 | -------------------------------------------------------------------------------- /migrations/versions/1418f098d06f_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 1418f098d06f 4 | Revises: 65b6acc8e745 5 | 6 | """ 7 | from alembic import op 8 | import sqlalchemy as sa 9 | 10 | 11 | # revision identifiers, used by Alembic. 12 | revision = '1418f098d06f' 13 | down_revision = '65b6acc8e745' 14 | branch_labels = None 15 | depends_on = None 16 | 17 | 18 | def upgrade(): 19 | # ### commands auto generated by Alembic - please adjust! ### 20 | op.create_index(op.f('ix_checker_files_file_hash'), 'checker_files', ['file_hash'], unique=False) 21 | op.create_index(op.f('ix_checker_filesystem_package'), 'checker_filesystem', ['package'], unique=False) 22 | op.create_index(op.f('ix_checker_results_status'), 'checker_results', ['status'], unique=False) 23 | # ### end Alembic commands ### 24 | 25 | 26 | def downgrade(): 27 | # ### commands auto generated by Alembic - please adjust! ### 28 | op.drop_index(op.f('ix_checker_results_status'), table_name='checker_results') 29 | op.drop_index(op.f('ix_checker_filesystem_package'), table_name='checker_filesystem') 30 | op.drop_index(op.f('ix_checker_files_file_hash'), table_name='checker_files') 31 | # ### end Alembic commands ### 32 | -------------------------------------------------------------------------------- /migrations/versions/1eeeeb5988b0_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 1eeeeb5988b0 4 | Revises: 10a5ee20d0ef 5 | 6 | """ 7 | from alembic import op 8 | import sqlalchemy as sa 9 | 10 | 11 | # revision identifiers, used by Alembic. 12 | revision = '1eeeeb5988b0' 13 | down_revision = '10a5ee20d0ef' 14 | branch_labels = None 15 | depends_on = None 16 | 17 | 18 | def upgrade(): 19 | # ### commands auto generated by Alembic - please adjust! ### 20 | op.create_index(op.f('ix_team_traffic_stats_time'), 'team_traffic_stats', ['time'], unique=False) 21 | op.create_unique_constraint('team_traffic_stats_unique_1', 'team_traffic_stats', ['time', 'team_id']) 22 | # ### end Alembic commands ### 23 | 24 | 25 | def downgrade(): 26 | # ### commands auto generated by Alembic - please adjust! ### 27 | op.drop_constraint('team_traffic_stats_unique_1', 'team_traffic_stats', type_='unique') 28 | op.drop_index(op.f('ix_team_traffic_stats_time'), table_name='team_traffic_stats') 29 | # ### end Alembic commands ### 30 | -------------------------------------------------------------------------------- /migrations/versions/251d7bd3641c_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 251d7bd3641c 4 | Revises: b2b35103cfec 5 | Create Date: 2022-03-02 01:04:44.614548 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '251d7bd3641c' 14 | down_revision = 'b2b35103cfec' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.add_column('teams', sa.Column('vpn2_connected', sa.Boolean(), server_default=sa.text('FALSE'), nullable=False)) 22 | # ### end Alembic commands ### 23 | 24 | 25 | def downgrade(): 26 | # ### commands auto generated by Alembic - please adjust! ### 27 | op.drop_column('teams', 'vpn2_connected') 28 | # ### end Alembic commands ### 29 | -------------------------------------------------------------------------------- /migrations/versions/28250d12516f_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 28250d12516f 4 | Revises: 4225a3d6edbd 5 | Create Date: 2020-06-22 20:47:59.961361 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '28250d12516f' 14 | down_revision = '4225a3d6edbd' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.add_column('teams', sa.Column('vpn_connection_count', sa.Integer(), server_default=sa.text('0'), nullable=False)) 22 | # ### end Alembic commands ### 23 | 24 | 25 | def downgrade(): 26 | # ### commands auto generated by Alembic - please adjust! ### 27 | op.drop_column('teams', 'vpn_connection_count') 28 | # ### end Alembic commands ### 29 | -------------------------------------------------------------------------------- /migrations/versions/2bcc2a3e63ea_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 2bcc2a3e63ea 4 | Revises: f022f2589997 5 | 6 | """ 7 | from alembic import op 8 | import sqlalchemy as sa 9 | 10 | 11 | # revision identifiers, used by Alembic. 12 | revision = '2bcc2a3e63ea' 13 | down_revision = 'f022f2589997' 14 | branch_labels = None 15 | depends_on = None 16 | 17 | 18 | def upgrade(): 19 | # ### commands auto generated by Alembic - please adjust! ### 20 | op.create_table('team_logos', 21 | sa.Column('id', sa.Integer(), nullable=False), 22 | sa.Column('hash', sa.String(length=64), nullable=False), 23 | sa.Column('content', sa.LargeBinary(), nullable=False), 24 | sa.PrimaryKeyConstraint('id') 25 | ) 26 | op.create_index(op.f('ix_team_logos_hash'), 'team_logos', ['hash'], unique=True) 27 | # ### end Alembic commands ### 28 | 29 | 30 | def downgrade(): 31 | # ### commands auto generated by Alembic - please adjust! ### 32 | op.drop_index(op.f('ix_team_logos_hash'), table_name='team_logos') 33 | op.drop_table('team_logos') 34 | # ### end Alembic commands ### 35 | -------------------------------------------------------------------------------- /migrations/versions/2f793eb41f0e_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 2f793eb41f0e 4 | Revises: 4a67c6f412ac 5 | 6 | """ 7 | from alembic import op 8 | import sqlalchemy as sa 9 | 10 | 11 | # revision identifiers, used by Alembic. 12 | revision = '2f793eb41f0e' 13 | down_revision = '4a67c6f412ac' 14 | branch_labels = None 15 | depends_on = None 16 | 17 | 18 | def upgrade(): 19 | # ### commands auto generated by Alembic - please adjust! ### 20 | op.add_column('checker_results', sa.Column('run_over_time', sa.Boolean(), server_default=sa.text('FALSE'), nullable=False)) 21 | # ### end Alembic commands ### 22 | 23 | 24 | def downgrade(): 25 | # ### commands auto generated by Alembic - please adjust! ### 26 | op.drop_column('checker_results', 'run_over_time') 27 | # ### end Alembic commands ### 28 | -------------------------------------------------------------------------------- /migrations/versions/4225a3d6edbd_add_timezone_info.py: -------------------------------------------------------------------------------- 1 | """add timezone info 2 | 3 | Revision ID: 4225a3d6edbd 4 | Revises: ddd894b2c20c 5 | 6 | """ 7 | from alembic import op 8 | import sqlalchemy as sa 9 | 10 | 11 | # revision identifiers, used by Alembic. 12 | revision = '4225a3d6edbd' 13 | down_revision = 'ddd894b2c20c' 14 | branch_labels = None 15 | depends_on = None 16 | 17 | 18 | def upgrade(): 19 | op.alter_column('checker_results', 'finished', type_=sa.TIMESTAMP(timezone=True)) 20 | op.alter_column('logmessages', 'created', type_=sa.TIMESTAMP(timezone=True)) 21 | op.alter_column('submitted_flags', 'ts', type_=sa.TIMESTAMP(timezone=True)) 22 | op.alter_column('teams', 'vpn_last_connect', type_=sa.TIMESTAMP(timezone=True)) 23 | op.alter_column('teams', 'vpn_last_disconnect', type_=sa.TIMESTAMP(timezone=True)) 24 | op.alter_column('team_traffic_stats', 'time', type_=sa.TIMESTAMP(timezone=True)) 25 | pass 26 | 27 | 28 | def downgrade(): 29 | pass 30 | -------------------------------------------------------------------------------- /migrations/versions/55a062b550bf_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 55a062b550bf 4 | Revises: 28250d12516f 5 | Create Date: 2021-04-24 00:44:49.502457 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '55a062b550bf' 14 | down_revision = '28250d12516f' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.add_column('team_traffic_stats', sa.Column('forward_self_bytes', sa.BigInteger(), nullable=False)) 22 | op.add_column('team_traffic_stats', sa.Column('forward_self_packets', sa.BigInteger(), nullable=False)) 23 | op.add_column('team_traffic_stats', sa.Column('forward_self_syn_acks', sa.BigInteger(), nullable=False)) 24 | op.add_column('team_traffic_stats', sa.Column('forward_self_syns', sa.BigInteger(), nullable=False)) 25 | # ### end Alembic commands ### 26 | 27 | 28 | def downgrade(): 29 | # ### commands auto generated by Alembic - please adjust! ### 30 | op.drop_column('team_traffic_stats', 'forward_self_syns') 31 | op.drop_column('team_traffic_stats', 'forward_self_syn_acks') 32 | op.drop_column('team_traffic_stats', 'forward_self_packets') 33 | op.drop_column('team_traffic_stats', 'forward_self_bytes') 34 | # ### end Alembic commands ### 35 | -------------------------------------------------------------------------------- /migrations/versions/5950403ab77d_add_ports_to_service_table.py: -------------------------------------------------------------------------------- 1 | """Add ports to service table 2 | 3 | Revision ID: 5950403ab77d 4 | Revises: 6a3f046884d6 5 | Create Date: 2024-11-11 14:33:40.895218 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '5950403ab77d' 14 | down_revision = '6a3f046884d6' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.add_column('services', sa.Column('ports', sa.String(), server_default=sa.text("''"), nullable=False)) 22 | # ### end Alembic commands ### 23 | 24 | 25 | def downgrade(): 26 | # ### commands auto generated by Alembic - please adjust! ### 27 | op.drop_column('services', 'ports') 28 | # ### end Alembic commands ### 29 | -------------------------------------------------------------------------------- /migrations/versions/5e6a03306c6e_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 5e6a03306c6e 4 | Revises: 86dd27c83676 5 | 6 | """ 7 | from alembic import op 8 | import sqlalchemy as sa 9 | 10 | 11 | # revision identifiers, used by Alembic. 12 | revision = '5e6a03306c6e' 13 | down_revision = '86dd27c83676' 14 | branch_labels = None 15 | depends_on = None 16 | 17 | 18 | def upgrade(): 19 | # ### commands auto generated by Alembic - please adjust! ### 20 | op.create_index(op.f('ix_submitted_flags_round_submitted'), 'submitted_flags', ['round_submitted'], unique=False) 21 | # ### end Alembic commands ### 22 | 23 | 24 | def downgrade(): 25 | # ### commands auto generated by Alembic - please adjust! ### 26 | op.drop_index(op.f('ix_submitted_flags_round_submitted'), table_name='submitted_flags') 27 | # ### end Alembic commands ### 28 | -------------------------------------------------------------------------------- /migrations/versions/6417a3f49101_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 6417a3f49101 4 | Revises: aea68a3e74a8 5 | 6 | """ 7 | from alembic import op 8 | import sqlalchemy as sa 9 | 10 | 11 | # revision identifiers, used by Alembic. 12 | revision = '6417a3f49101' 13 | down_revision = 'aea68a3e74a8' 14 | branch_labels = None 15 | depends_on = None 16 | 17 | 18 | def upgrade(): 19 | # ### commands auto generated by Alembic - please adjust! ### 20 | op.add_column('submitted_flags', sa.Column('is_firstblood', sa.Boolean(), server_default=sa.text('FALSE'), nullable=False)) 21 | op.create_index(op.f('ix_submitted_flags_is_firstblood'), 'submitted_flags', ['is_firstblood'], unique=False) 22 | # ### end Alembic commands ### 23 | 24 | 25 | def downgrade(): 26 | # ### commands auto generated by Alembic - please adjust! ### 27 | op.drop_index(op.f('ix_submitted_flags_is_firstblood'), table_name='submitted_flags') 28 | op.drop_column('submitted_flags', 'is_firstblood') 29 | # ### end Alembic commands ### 30 | -------------------------------------------------------------------------------- /migrations/versions/65b6acc8e745_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 65b6acc8e745 4 | Revises: a2e99d5c2195 5 | 6 | """ 7 | from alembic import op 8 | import sqlalchemy as sa 9 | 10 | 11 | # revision identifiers, used by Alembic. 12 | revision = '65b6acc8e745' 13 | down_revision = 'a2e99d5c2195' 14 | branch_labels = None 15 | depends_on = None 16 | 17 | 18 | def upgrade(): 19 | # ### commands auto generated by Alembic - please adjust! ### 20 | op.create_index(op.f('ix_team_points_round'), 'team_points', ['round'], unique=False) 21 | op.create_index(op.f('ix_team_rankings_round'), 'team_rankings', ['round'], unique=False) 22 | # ### end Alembic commands ### 23 | 24 | 25 | def downgrade(): 26 | # ### commands auto generated by Alembic - please adjust! ### 27 | op.drop_index(op.f('ix_team_rankings_round'), table_name='team_rankings') 28 | op.drop_index(op.f('ix_team_points_round'), table_name='team_points') 29 | # ### end Alembic commands ### 30 | -------------------------------------------------------------------------------- /migrations/versions/6a3f046884d6_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 6a3f046884d6 4 | Revises: 251d7bd3641c 5 | Create Date: 2022-05-19 11:26:41.939234 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | from sqlalchemy.dialects import postgresql 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '6a3f046884d6' 14 | down_revision = '251d7bd3641c' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | op.alter_column('services', 'flags_per_round', existing_type=sa.Integer(), type_=sa.Float()) 21 | 22 | 23 | def downgrade(): 24 | op.alter_column('services', 'flags_per_round', existing_type=sa.Float(), type_=sa.Integer()) 25 | -------------------------------------------------------------------------------- /migrations/versions/7aab1b106d02_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 7aab1b106d02 4 | Revises: e50efd6bd399 5 | 6 | """ 7 | from alembic import op 8 | import sqlalchemy as sa 9 | 10 | 11 | # revision identifiers, used by Alembic. 12 | revision = '7aab1b106d02' 13 | down_revision = 'e50efd6bd399' 14 | branch_labels = None 15 | depends_on = None 16 | 17 | 18 | def upgrade(): 19 | # ### commands auto generated by Alembic - please adjust! ### 20 | op.add_column('services', sa.Column('flag_ids', sa.String(length=128), server_default=sa.text('NULL'), nullable=True)) 21 | # ### end Alembic commands ### 22 | 23 | 24 | def downgrade(): 25 | # ### commands auto generated by Alembic - please adjust! ### 26 | op.drop_column('services', 'flag_ids') 27 | # ### end Alembic commands ### 28 | -------------------------------------------------------------------------------- /migrations/versions/86dd27c83676_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 86dd27c83676 4 | Revises: 1418f098d06f 5 | 6 | """ 7 | from alembic import op 8 | import sqlalchemy as sa 9 | 10 | 11 | # revision identifiers, used by Alembic. 12 | revision = '86dd27c83676' 13 | down_revision = '1418f098d06f' 14 | branch_labels = None 15 | depends_on = None 16 | 17 | 18 | def upgrade(): 19 | # ### commands auto generated by Alembic - please adjust! ### 20 | op.execute('ALTER TABLE submitted_flags RENAME round TO round_submitted') 21 | # op.execute('ALTER INDEX IF EXISTS ix_submitted_flags_round RENAME TO ix_submitted_flags_round_submitted') 22 | op.execute('DROP INDEX IF EXISTS ix_submitted_flags_round') 23 | # ### end Alembic commands ### 24 | 25 | 26 | def downgrade(): 27 | # ### commands auto generated by Alembic - please adjust! ### 28 | op.execute('ALTER TABLE submitted_flags RENAME round_submitted TO round') 29 | op.execute('ALTER INDEX IF EXISTS ix_submitted_flags_round_submitted RENAME TO ix_submitted_flags_round') 30 | # ### end Alembic commands ### 31 | -------------------------------------------------------------------------------- /migrations/versions/922cc6992093_add_wireguard_status_to_teams.py: -------------------------------------------------------------------------------- 1 | """Add wireguard status to teams 2 | 3 | Revision ID: 922cc6992093 4 | Revises: 5950403ab77d 5 | Create Date: 2024-11-24 00:54:52.897263 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '922cc6992093' 14 | down_revision = '5950403ab77d' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.add_column('teams', sa.Column('wg_vulnbox_connected', sa.Boolean(), server_default=sa.text('FALSE'), nullable=False)) 22 | op.add_column('teams', sa.Column('wg_boxes_connected', sa.Boolean(), server_default=sa.text('FALSE'), nullable=False)) 23 | # ### end Alembic commands ### 24 | 25 | 26 | def downgrade(): 27 | # ### commands auto generated by Alembic - please adjust! ### 28 | op.drop_column('teams', 'wg_boxes_connected') 29 | op.drop_column('teams', 'wg_vulnbox_connected') 30 | # ### end Alembic commands ### 31 | -------------------------------------------------------------------------------- /migrations/versions/9676f8be1930_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 9676f8be1930 4 | Revises: c84ad4bd6649 5 | 6 | """ 7 | from alembic import op 8 | import sqlalchemy as sa 9 | 10 | 11 | # revision identifiers, used by Alembic. 12 | revision = '9676f8be1930' 13 | down_revision = 'c84ad4bd6649' 14 | branch_labels = None 15 | depends_on = None 16 | 17 | 18 | def upgrade(): 19 | # ### commands auto generated by Alembic - please adjust! ### 20 | op.drop_constraint('submitted_flags_unique_1', 'submitted_flags', type_='unique') 21 | op.create_unique_constraint('submitted_flags_unique_1', 'submitted_flags', ['submitted_by', 'team_id', 'service_id', 'expires', 'payload']) 22 | # ### end Alembic commands ### 23 | 24 | 25 | def downgrade(): 26 | # ### commands auto generated by Alembic - please adjust! ### 27 | op.drop_constraint('submitted_flags_unique_1', 'submitted_flags', type_='unique') 28 | op.create_unique_constraint('submitted_flags_unique_1', 'submitted_flags', ['submitted_by', 'team_id', 'service_id', 'expires']) 29 | # ### end Alembic commands ### 30 | -------------------------------------------------------------------------------- /migrations/versions/a2e99d5c2195_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: a2e99d5c2195 4 | Revises: 9676f8be1930 5 | 6 | """ 7 | from alembic import op 8 | import sqlalchemy as sa 9 | 10 | 11 | # revision identifiers, used by Alembic. 12 | revision = 'a2e99d5c2195' 13 | down_revision = '9676f8be1930' 14 | branch_labels = None 15 | depends_on = None 16 | 17 | 18 | def upgrade(): 19 | # ### commands auto generated by Alembic - please adjust! ### 20 | op.create_index(op.f('ix_checker_results_round'), 'checker_results', ['round'], unique=False) 21 | op.create_index(op.f('ix_submitted_flags_round'), 'submitted_flags', ['round'], unique=False) 22 | # ### end Alembic commands ### 23 | 24 | 25 | def downgrade(): 26 | # ### commands auto generated by Alembic - please adjust! ### 27 | op.drop_index(op.f('ix_submitted_flags_round'), table_name='submitted_flags') 28 | op.drop_index(op.f('ix_checker_results_round'), table_name='checker_results') 29 | # ### end Alembic commands ### 30 | -------------------------------------------------------------------------------- /migrations/versions/aea68a3e74a8_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: aea68a3e74a8 4 | Revises: 1eeeeb5988b0 5 | 6 | """ 7 | from alembic import op 8 | import sqlalchemy as sa 9 | 10 | 11 | # revision identifiers, used by Alembic. 12 | revision = 'aea68a3e74a8' 13 | down_revision = '1eeeeb5988b0' 14 | branch_labels = None 15 | depends_on = None 16 | 17 | 18 | def upgrade(): 19 | # ### commands auto generated by Alembic - please adjust! ### 20 | op.add_column('services', sa.Column('num_payloads', sa.Integer(), server_default=sa.text('0'), nullable=False)) 21 | # ### end Alembic commands ### 22 | 23 | 24 | def downgrade(): 25 | # ### commands auto generated by Alembic - please adjust! ### 26 | op.drop_column('services', 'num_payloads') 27 | # ### end Alembic commands ### 28 | -------------------------------------------------------------------------------- /migrations/versions/b216b27a5b41_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: b216b27a5b41 4 | Revises: 5e6a03306c6e 5 | 6 | """ 7 | from alembic import op 8 | import sqlalchemy as sa 9 | 10 | 11 | # revision identifiers, used by Alembic. 12 | revision = 'b216b27a5b41' 13 | down_revision = '5e6a03306c6e' 14 | branch_labels = None 15 | depends_on = None 16 | 17 | 18 | def upgrade(): 19 | # ### commands auto generated by Alembic - please adjust! ### 20 | op.add_column('submitted_flags', sa.Column('round_issued', sa.SmallInteger(), nullable=False)) 21 | op.drop_constraint('submitted_flags_unique_1', 'submitted_flags', type_='unique') 22 | op.create_unique_constraint('submitted_flags_unique_1', 'submitted_flags', ['submitted_by', 'team_id', 'service_id', 'round_issued', 'payload']) 23 | op.drop_column('submitted_flags', 'expires') 24 | # ### end Alembic commands ### 25 | 26 | 27 | def downgrade(): 28 | # ### commands auto generated by Alembic - please adjust! ### 29 | op.add_column('submitted_flags', sa.Column('expires', sa.INTEGER(), autoincrement=False, nullable=False)) 30 | op.drop_constraint('submitted_flags_unique_1', 'submitted_flags', type_='unique') 31 | op.create_unique_constraint('submitted_flags_unique_1', 'submitted_flags', ['submitted_by', 'team_id', 'service_id', 'expires', 'payload']) 32 | op.drop_column('submitted_flags', 'round_issued') 33 | # ### end Alembic commands ### 34 | -------------------------------------------------------------------------------- /migrations/versions/b2b35103cfec_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: b2b35103cfec 4 | Revises: 55a062b550bf 5 | Create Date: 2021-04-25 19:07:23.139711 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'b2b35103cfec' 14 | down_revision = '55a062b550bf' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.add_column('services', sa.Column('checker_route', sa.String(length=64), server_default=sa.text('NULL'), nullable=True)) 22 | # ### end Alembic commands ### 23 | 24 | 25 | def downgrade(): 26 | # ### commands auto generated by Alembic - please adjust! ### 27 | op.drop_column('services', 'checker_route') 28 | # ### end Alembic commands ### 29 | -------------------------------------------------------------------------------- /migrations/versions/b80816ee51bf_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: b80816ee51bf 4 | Revises: 2f793eb41f0e 5 | 6 | """ 7 | from alembic import op 8 | import sqlalchemy as sa 9 | 10 | 11 | # revision identifiers, used by Alembic. 12 | revision = 'b80816ee51bf' 13 | down_revision = '2f793eb41f0e' 14 | branch_labels = None 15 | depends_on = None 16 | 17 | 18 | def upgrade(): 19 | # ### commands auto generated by Alembic - please adjust! ### 20 | op.create_table('checker_files', 21 | sa.Column('id', sa.Integer(), nullable=False), 22 | sa.Column('file_hash', sa.String(length=32), nullable=False), 23 | sa.Column('content', sa.LargeBinary(), nullable=False), 24 | sa.PrimaryKeyConstraint('id'), 25 | sa.UniqueConstraint('file_hash', name='checker_files_unique_1') 26 | ) 27 | op.create_table('checker_filesystem', 28 | sa.Column('id', sa.Integer(), nullable=False), 29 | sa.Column('package', sa.String(length=32), nullable=False), 30 | sa.Column('path', sa.String(), nullable=False), 31 | sa.Column('file_hash', sa.String(length=32), nullable=True), 32 | sa.PrimaryKeyConstraint('id'), 33 | sa.UniqueConstraint('package', 'path', name='checker_filesystem_unique_1') 34 | ) 35 | op.add_column('services', sa.Column('package', sa.String(length=32), nullable=True)) 36 | # ### end Alembic commands ### 37 | 38 | 39 | def downgrade(): 40 | # ### commands auto generated by Alembic - please adjust! ### 41 | op.drop_column('services', 'package') 42 | op.drop_table('checker_filesystem') 43 | op.drop_table('checker_files') 44 | # ### end Alembic commands ### 45 | -------------------------------------------------------------------------------- /migrations/versions/bead29c0348f_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: bead29c0348f 4 | Revises: c2c1a5d3b062 5 | 6 | """ 7 | from alembic import op 8 | import sqlalchemy as sa 9 | 10 | 11 | # revision identifiers, used by Alembic. 12 | revision = 'bead29c0348f' 13 | down_revision = 'c2c1a5d3b062' 14 | branch_labels = None 15 | depends_on = None 16 | 17 | 18 | def upgrade(): 19 | # ### commands auto generated by Alembic - please adjust! ### 20 | op.add_column('services', sa.Column('checker_enabled', sa.Boolean(), server_default=sa.text('TRUE'), nullable=False)) 21 | # ### end Alembic commands ### 22 | 23 | 24 | def downgrade(): 25 | # ### commands auto generated by Alembic - please adjust! ### 26 | op.drop_column('services', 'checker_enabled') 27 | # ### end Alembic commands ### 28 | -------------------------------------------------------------------------------- /migrations/versions/c2c1a5d3b062_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: c2c1a5d3b062 4 | Revises: b80816ee51bf 5 | 6 | """ 7 | from alembic import op 8 | import sqlalchemy as sa 9 | 10 | 11 | # revision identifiers, used by Alembic. 12 | revision = 'c2c1a5d3b062' 13 | down_revision = 'b80816ee51bf' 14 | branch_labels = None 15 | depends_on = None 16 | 17 | 18 | def upgrade(): 19 | # ### commands auto generated by Alembic - please adjust! ### 20 | op.add_column('services', sa.Column('checker_script_dir', sa.String(), server_default=sa.text('NULL'), nullable=True)) 21 | # ### end Alembic commands ### 22 | 23 | 24 | def downgrade(): 25 | # ### commands auto generated by Alembic - please adjust! ### 26 | op.drop_column('services', 'checker_script_dir') 27 | # ### end Alembic commands ### 28 | -------------------------------------------------------------------------------- /migrations/versions/c84ad4bd6649_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: c84ad4bd6649 4 | Revises: 057792e3ba77 5 | 6 | """ 7 | from alembic import op 8 | import sqlalchemy as sa 9 | 10 | 11 | # revision identifiers, used by Alembic. 12 | 13 | revision = 'c84ad4bd6649' 14 | down_revision = '057792e3ba77' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.add_column('submitted_flags', sa.Column('payload', sa.Integer(), nullable=False, server_default=sa.text('0'))) 22 | # ### end Alembic commands ### 23 | 24 | 25 | def downgrade(): 26 | # ### commands auto generated by Alembic - please adjust! ### 27 | op.drop_column('submitted_flags', 'payload') 28 | # ### end Alembic commands ### 29 | -------------------------------------------------------------------------------- /migrations/versions/d2f5aba10faa_record_tick_information_in_db.py: -------------------------------------------------------------------------------- 1 | """record tick information in DB 2 | 3 | Revision ID: d2f5aba10faa 4 | Revises: d3f46a7baef6 5 | Create Date: 2024-12-12 23:20:10.845605 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | from sqlalchemy.dialects import postgresql 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'd2f5aba10faa' 14 | down_revision = 'd3f46a7baef6' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.create_table('ticks', 22 | sa.Column('tick', sa.Integer(), nullable=False), 23 | sa.Column('start', sa.TIMESTAMP(timezone=True), server_default=sa.text('NULL'), nullable=True), 24 | sa.Column('end', sa.TIMESTAMP(timezone=True), server_default=sa.text('NULL'), nullable=True), 25 | sa.PrimaryKeyConstraint('tick') 26 | ) 27 | # ### end Alembic commands ### 28 | 29 | 30 | def downgrade(): 31 | # ### commands auto generated by Alembic - please adjust! ### 32 | op.drop_table('ticks') 33 | # ### end Alembic commands ### 34 | -------------------------------------------------------------------------------- /migrations/versions/ddd894b2c20c_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: ddd894b2c20c 4 | Revises: 2bcc2a3e63ea 5 | 6 | """ 7 | from alembic import op 8 | import sqlalchemy as sa 9 | 10 | 11 | # revision identifiers, used by Alembic. 12 | revision = 'ddd894b2c20c' 13 | down_revision = '2bcc2a3e63ea' 14 | branch_labels = None 15 | depends_on = None 16 | 17 | 18 | def upgrade(): 19 | # ### commands auto generated by Alembic - please adjust! ### 20 | op.add_column('services', sa.Column('setup_package', sa.String(length=32), server_default=sa.text('NULL'), nullable=True)) 21 | # ### end Alembic commands ### 22 | 23 | 24 | def downgrade(): 25 | # ### commands auto generated by Alembic - please adjust! ### 26 | op.drop_column('services', 'setup_package') 27 | # ### end Alembic commands ### 28 | -------------------------------------------------------------------------------- /migrations/versions/e50efd6bd399_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: e50efd6bd399 4 | Revises: 6417a3f49101 5 | 6 | """ 7 | from alembic import op 8 | import sqlalchemy as sa 9 | 10 | # revision identifiers, used by Alembic. 11 | revision = 'e50efd6bd399' 12 | down_revision = '6417a3f49101' 13 | branch_labels = None 14 | depends_on = None 15 | 16 | 17 | def upgrade(): 18 | # ### commands auto generated by Alembic - please adjust! ### 19 | op.add_column('team_points', sa.Column('off_points', sa.Float(), nullable=False)) 20 | op.add_column('team_points', sa.Column('def_points', sa.Float(), nullable=False)) 21 | op.alter_column('team_points', 'flag_points', nullable=False, new_column_name='flag_captured_count') 22 | op.add_column('team_points', sa.Column('flag_stolen_count', sa.Integer(), nullable=False)) 23 | op.alter_column('team_points', 'sla_points', existing_type=sa.Integer, type_=sa.Float, existing_nullable=False) 24 | op.alter_column('team_rankings', 'points', existing_type=sa.Integer, type_=sa.Float, existing_nullable=False) 25 | # ### end Alembic commands ### 26 | 27 | 28 | def downgrade(): 29 | # ### commands auto generated by Alembic - please adjust! ### 30 | #op.add_column('team_points', sa.Column('flag_points', sa.INTEGER(), autoincrement=False, nullable=False)) 31 | op.drop_column('team_points', 'off_points') 32 | op.drop_column('team_points', 'def_points') 33 | op.alter_column('team_points', 'flag_captured_count', nullable=False, new_column_name='flag_points') 34 | op.drop_column('team_points', 'flag_stolen_count') 35 | op.alter_column('team_points', 'sla_points', existing_type=sa.Float, type_=sa.Integer, existing_nullable=False) 36 | # ### end Alembic commands ### 37 | -------------------------------------------------------------------------------- /migrations/versions/e94f0d7b3dcc_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: e94f0d7b3dcc 4 | Revises: f764e8e9fbb1 5 | 6 | """ 7 | from alembic import op 8 | import sqlalchemy as sa 9 | 10 | 11 | # revision identifiers, used by Alembic. 12 | revision = 'e94f0d7b3dcc' 13 | down_revision = 'f764e8e9fbb1' 14 | branch_labels = None 15 | depends_on = None 16 | 17 | 18 | def upgrade(): 19 | # ### commands auto generated by Alembic - please adjust! ### 20 | op.add_column('services', sa.Column('flags_per_round', sa.Integer(), server_default=sa.text('1'), nullable=False)) 21 | # ### end Alembic commands ### 22 | 23 | 24 | def downgrade(): 25 | # ### commands auto generated by Alembic - please adjust! ### 26 | op.drop_column('services', 'flags_per_round') 27 | # ### end Alembic commands ### 28 | -------------------------------------------------------------------------------- /migrations/versions/empty: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarkusBauer/saarctf-gameserver/88bee31ca3bf56492714eac5d0c6f96bc1a92079/migrations/versions/empty -------------------------------------------------------------------------------- /migrations/versions/f022f2589997_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: f022f2589997 4 | Revises: e94f0d7b3dcc 5 | 6 | """ 7 | from alembic import op 8 | import sqlalchemy as sa 9 | 10 | 11 | # revision identifiers, used by Alembic. 12 | revision = 'f022f2589997' 13 | down_revision = 'e94f0d7b3dcc' 14 | branch_labels = None 15 | depends_on = None 16 | 17 | 18 | def upgrade(): 19 | # ### commands auto generated by Alembic - please adjust! ### 20 | op.add_column('teams', sa.Column('affiliation', sa.String(length=128), server_default=sa.text('NULL'), nullable=True)) 21 | op.add_column('teams', sa.Column('logo', sa.String(length=64), server_default=sa.text('NULL'), nullable=True)) 22 | op.add_column('teams', sa.Column('website', sa.String(length=128), server_default=sa.text('NULL'), nullable=True)) 23 | # ### end Alembic commands ### 24 | 25 | 26 | def downgrade(): 27 | # ### commands auto generated by Alembic - please adjust! ### 28 | op.drop_column('teams', 'website') 29 | op.drop_column('teams', 'logo') 30 | op.drop_column('teams', 'affiliation') 31 | # ### end Alembic commands ### 32 | -------------------------------------------------------------------------------- /migrations/versions/f764e8e9fbb1_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: f764e8e9fbb1 4 | Revises: 7aab1b106d02 5 | 6 | """ 7 | from alembic import op 8 | import sqlalchemy as sa 9 | 10 | 11 | # revision identifiers, used by Alembic. 12 | revision = 'f764e8e9fbb1' 13 | down_revision = '7aab1b106d02' 14 | branch_labels = None 15 | depends_on = None 16 | 17 | 18 | def upgrade(): 19 | # ### commands auto generated by Alembic - please adjust! ### 20 | op.add_column('team_points', sa.Column('sla_delta', sa.Float(), nullable=False)) 21 | # ### end Alembic commands ### 22 | 23 | 24 | def downgrade(): 25 | # ### commands auto generated by Alembic - please adjust! ### 26 | op.drop_column('team_points', 'sla_delta') 27 | # ### end Alembic commands ### 28 | -------------------------------------------------------------------------------- /migrations/versions/fd06fc8ae2cb_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: fd06fc8ae2cb 4 | Revises: 0e400cc111e2 5 | 6 | """ 7 | from alembic import op 8 | import sqlalchemy as sa 9 | 10 | 11 | # revision identifiers, used by Alembic. 12 | revision = 'fd06fc8ae2cb' 13 | down_revision = '0e400cc111e2' 14 | branch_labels = None 15 | depends_on = None 16 | 17 | 18 | def upgrade(): 19 | # ### commands auto generated by Alembic - please adjust! ### 20 | op.add_column('teams', sa.Column('vpn_connected', sa.Boolean(), server_default=sa.text('FALSE'), nullable=False)) 21 | op.add_column('teams', sa.Column('vpn_last_connect', sa.TIMESTAMP(), server_default=sa.text('NULL'), nullable=True)) 22 | op.add_column('teams', sa.Column('vpn_last_disconnect', sa.TIMESTAMP(), server_default=sa.text('NULL'), nullable=True)) 23 | # ### end Alembic commands ### 24 | 25 | 26 | def downgrade(): 27 | # ### commands auto generated by Alembic - please adjust! ### 28 | op.drop_column('teams', 'vpn_last_disconnect') 29 | op.drop_column('teams', 'vpn_last_connect') 30 | op.drop_column('teams', 'vpn_connected') 31 | # ### end Alembic commands ### 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "saarctf-framework", 3 | "version": "1.0.0", 4 | "description": "saarCTF Gameserver Framework", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1", 7 | "prebuild": "mkdirp controlserver/static/vendor", 8 | "less": "lessc --include-path=node_modules controlserver/static/less/index.less controlserver/static/css/index.css", 9 | "copy": "copyfiles -u 3 \"node_modules/bootstrap/dist/*/**\" controlserver/static/vendor && copyfiles -u 3 \"node_modules/bootstrap/dist/fonts/**\" controlserver/static/ && copyfiles -u 2 node_modules/angular/angular.js controlserver/static/vendor/js/ && copyfiles -u 3 node_modules/jquery/dist/jquery.js controlserver/static/vendor/js/ && copyfiles -u 3 \"node_modules/eonasdan-bootstrap-datetimepicker/build/*/**\" controlserver/static/vendor/ && copyfiles -u 3 node_modules/moment/min/moment-with-locales.min.js controlserver/static/vendor/js/ && copyfiles -u 3 node_modules/moment-duration-format/lib/moment-duration-format.js controlserver/static/vendor/js/ && copyfiles -u 3 node_modules/chart.js/dist/Chart.bundle.min.js controlserver/static/vendor/js/ && copyfiles -u 3 node_modules/angular-chart.js/dist/angular-chart.min.js controlserver/static/vendor/js/", 10 | "build": "npm run copy && npm run less" 11 | }, 12 | "author": "saarsec", 13 | "dependencies": { 14 | "angular": "^1.8.0", 15 | "angular-chart.js": "^1.1.1", 16 | "bootstrap": "^3.4.1", 17 | "chart.js": "^2.9.3", 18 | "copyfiles": "^2.3.0", 19 | "eonasdan-bootstrap-datetimepicker": "^4.17.47", 20 | "jquery": "^3.5.0", 21 | "less": "^3.12.2", 22 | "mkdirp": "^1.0.4", 23 | "moment-duration-format": "^2.3.2" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | mypy>=1.5.1 2 | types-PyYAML 3 | types-redis 4 | types-requests 5 | types-selenium 6 | types-setuptools 7 | types-six 8 | types-ujson 9 | pytest 10 | -------------------------------------------------------------------------------- /requirements-script.txt: -------------------------------------------------------------------------------- 1 | # for the task runner 2 | flask<3 3 | psycopg2-binary 4 | redis 5 | celery[ampq,redis]>=5.3, <6 6 | filelock 7 | 8 | # for the scripts 9 | requests 10 | pwntools 11 | numpy 12 | pycryptodome 13 | fuckpy3 14 | beautifulsoup4 15 | pytz 16 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | flask 2 | flask_admin 3 | sqlalchemy[mypy]>=2, <3 4 | alembic 5 | psycopg2-binary 6 | redis 7 | celery[amqp,redis]>=5.3, <6 8 | flower>=2, <3 9 | setproctitle 10 | filelock 11 | ujson 12 | htmlmin2 13 | requests 14 | Pillow 15 | hcloud>=1.16.0 16 | aiohttp>=3.11.7 17 | python-dotenv 18 | pyroute2 19 | cryptography>=43.0.3 20 | filelock 21 | ecs-logging 22 | pyyaml 23 | typing-extensions # for typing.override - drop when we have min 3.12 24 | # for enochecker framework 25 | aiohttp 26 | enochecker_core 27 | jsons 28 | -------------------------------------------------------------------------------- /run-mypy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Requirements: 4 | # pip install --upgrade -r requirements-dev.txt 5 | 6 | exec mypy --config-file mypy.ini --no-incremental controlserver/*.py scripts/*.py checker_runner/*.py gamelib/*.py vpn/*.py vpnboard/*.py 7 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | cd "`dirname "$0"`" 5 | 6 | # make --silent deps 7 | 8 | . venv/bin/activate 9 | exec venv/bin/python3 "$@" 10 | -------------------------------------------------------------------------------- /saarctf_commons/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarkusBauer/saarctf-gameserver/88bee31ca3bf56492714eac5d0c6f96bc1a92079/saarctf_commons/__init__.py -------------------------------------------------------------------------------- /saarctf_commons/db_utils.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import time 3 | from functools import wraps 4 | from typing import Callable, TypeVar, ParamSpec 5 | from sqlalchemy.exc import SQLAlchemyError 6 | 7 | T = TypeVar('T') 8 | P = ParamSpec('P') 9 | 10 | 11 | def retry_on_sql_error(attempts: int = 3, sleeptime: float = 0.5) -> Callable[[Callable[P, T]], Callable[P, T]]: 12 | def decorator(func: Callable[P, T]) -> Callable[P, T]: 13 | @wraps(func) 14 | def wrapper(*args: P.args, **kwargs: P.kwargs) -> T: 15 | failures = 0 16 | while True: 17 | try: 18 | result: T = func(*args, **kwargs) 19 | if failures > 0: 20 | logging.warning(f'Retry of {func.__name__} succeeded (attempt {failures + 1}/{attempts}).') 21 | return result 22 | except (SQLAlchemyError, ConnectionResetError) as e: 23 | failures += 1 24 | if failures < attempts: 25 | logging.warning( 26 | f'Retrying {func.__name__} (attempt {failures + 1}/{attempts}) after SQL error {str(e)}' 27 | ) 28 | time.sleep(sleeptime) 29 | else: 30 | raise e 31 | 32 | return wrapper 33 | 34 | return decorator 35 | -------------------------------------------------------------------------------- /saarctf_commons/logging_utils.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | 4 | 5 | class DefaultAttributesFilter(logging.Filter): 6 | def __init__(self, attributes: dict) -> None: 7 | super().__init__() 8 | self._default_attrs = attributes 9 | 10 | def filter(self, record): 11 | for k, v in self._default_attrs.items(): 12 | if not hasattr(record, k): 13 | setattr(record, k, v) 14 | return True 15 | 16 | 17 | def setup_script_logging(component_name: str, logfile: str | None = None) -> None: 18 | format: str = "%(asctime)s [%(levelname)s] %(message)s" 19 | logging.basicConfig(level=logging.INFO, format=format) 20 | logging.root.addFilter(DefaultAttributesFilter({"event.source": component_name})) 21 | 22 | if logfile is not None: 23 | fh = logging.FileHandler(logfile) 24 | fh.setLevel(logging.INFO) 25 | fh.setFormatter(logging.Formatter(format)) 26 | logging.root.addHandler(fh) 27 | 28 | add_ecs_logging() 29 | 30 | 31 | def add_ecs_logging() -> None: 32 | ecs_logfile = os.environ.get("ECS_LOGFILE", None) 33 | if ecs_logfile: 34 | import ecs_logging 35 | fh = logging.FileHandler(ecs_logfile) 36 | fh.setLevel(logging.INFO) 37 | fh.setFormatter(ecs_logging.StdlibFormatter()) 38 | logging.root.addHandler(fh) 39 | -------------------------------------------------------------------------------- /saarctf_commons/redis.py: -------------------------------------------------------------------------------- 1 | import redis 2 | 3 | from saarctf_commons.config import config 4 | 5 | 6 | class NamedRedisConnection(redis.Connection): 7 | name: str = '' 8 | 9 | def on_connect(self): 10 | redis.Connection.on_connect(self) 11 | if self.name: 12 | self.send_command("CLIENT SETNAME", self.name.replace(' ', '_')) 13 | self.read_response() 14 | 15 | @classmethod 16 | def set_clientname(cls, name: str, overwrite: bool = False) -> None: 17 | """ 18 | Set the name of all future redis connections. 19 | :param name: 20 | :param overwrite: Overwrite an already existing name? 21 | :return: 22 | """ 23 | if overwrite or not cls.name: 24 | cls.name = name 25 | 26 | 27 | redis_default_connection_pool: redis.ConnectionPool | None = None 28 | 29 | 30 | def get_redis_connection() -> redis.StrictRedis: 31 | """ 32 | :return: A new Redis connection (possibly from a connection pool). Name is already set. 33 | """ 34 | global redis_default_connection_pool 35 | if not redis_default_connection_pool: 36 | redis_default_connection_pool = redis.ConnectionPool(connection_class=NamedRedisConnection, **config.REDIS) 37 | r = redis.StrictRedis(connection_pool=redis_default_connection_pool) 38 | return r 39 | -------------------------------------------------------------------------------- /sample_files/Loadtest.md: -------------------------------------------------------------------------------- 1 | Setup 2 | ----- 3 | `mkdir /dev/shm/storage` 4 | - Setup `demoservice.php` on localhost (symlink in webroot might be enough) 5 | - Configure everything to use a seperate database / redis / rabbitmq 6 | - `flask db upgrade` , import large_ctf.sql 7 | - Start gameserver, flower, celery worker, flag submitter 8 | 9 | - Start the game 10 | - Start exploiters using many instances of `python sample_files/demo_exploit.py repeat` 11 | 12 | Useful commands 13 | --------------- 14 | Exploiter: 15 | `python demo_exploit.py repeat` 16 | 17 | Cleanup of storage: 18 | `find /dev/shm/storage -mmin +21 -type f -exec rm {} \;` 19 | 20 | Automatic cleanup of storage: 21 | `while true; do find /dev/shm/storage -mmin +21 -type f -exec rm {} \; ; sleep 120; done` 22 | 23 | Check size of Postgresql database tables: 24 | ```sql 25 | SELECT nspname || '.' || relname AS "relation", pg_size_pretty(pg_relation_size(C.oid)) AS "size", reltuples::bigint as "count" 26 | FROM pg_class C 27 | LEFT JOIN pg_namespace N ON (N.oid = C.relnamespace) 28 | WHERE nspname NOT IN ('pg_catalog', 'information_schema') 29 | ORDER BY pg_relation_size(C.oid) DESC 30 | LIMIT 20; 31 | ``` 32 | 33 | Total database size: 34 | ```sql 35 | SELECT pg_size_pretty(sum(pg_relation_size(C.oid))) AS "total_size" 36 | FROM pg_class C LEFT JOIN pg_namespace N ON (N.oid = C.relnamespace) 37 | WHERE nspname NOT IN ('pg_catalog', 'information_schema'); 38 | ``` 39 | 40 | 41 | Check Postgresql connections: 42 | `SELECT sum(numbackends) FROM pg_stat_database;` 43 | -------------------------------------------------------------------------------- /sample_files/checker_fnf/checker.py: -------------------------------------------------------------------------------- 1 | from gamelib import gamelib 2 | from . import test 3 | 4 | 5 | class SampleService(gamelib.ServiceInterface): 6 | def check_integrity(self, team, tick): 7 | if not test.VERSION == 1: 8 | raise Exception(test.VERSION) 9 | return True 10 | 11 | def store_flags(self, team, tick): 12 | # self.do_blocking_io() 13 | return 1 14 | 15 | def retrieve_flags(self, team, tick): 16 | # return 1 17 | raise gamelib.FlagMissingException("FLAG{} not found") 18 | 19 | def do_blocking_io(self): 20 | print('Block...') 21 | import socket 22 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 23 | sock.connect(('127.0.0.1', 12345)) 24 | print(sock.recv(1024)) 25 | -------------------------------------------------------------------------------- /sample_files/checker_fnf/test.py: -------------------------------------------------------------------------------- 1 | 2 | VERSION = 1 3 | -------------------------------------------------------------------------------- /sample_files/checker_ok/checker.py: -------------------------------------------------------------------------------- 1 | from gamelib import gamelib 2 | from . import test 3 | 4 | 5 | class SampleService(gamelib.ServiceInterface): 6 | def check_integrity(self, team, tick): 7 | if not test.VERSION == 2: 8 | raise Exception(test.VERSION) 9 | 10 | def store_flags(self, team, tick): 11 | pass 12 | 13 | def retrieve_flags(self, team, tick): 14 | pass 15 | -------------------------------------------------------------------------------- /sample_files/checker_ok/test.py: -------------------------------------------------------------------------------- 1 | 2 | VERSION = 2 3 | -------------------------------------------------------------------------------- /sample_files/demo_traffic.py: -------------------------------------------------------------------------------- 1 | import random 2 | import time 3 | from math import floor 4 | from typing import List 5 | import sys 6 | import os 7 | 8 | sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 9 | 10 | import controlserver.app 11 | from controlserver.models import db, TeamPoints, Team, TeamTrafficStats 12 | 13 | 14 | def random_subvec(old: List[int]): 15 | if not old: 16 | pc = random.randint(32, 5000) 17 | by = pc * random.randint(0x1000, 0xffff) 18 | syn = int(random.randint(5, 40) * pc / 100) 19 | synack = int(random.randint(30, 90) * syn / 100) 20 | return [pc, by, syn, synack] 21 | pc, by, syn, synack = old 22 | mod = random.uniform(0.8, 1.21) 23 | pc = int(round(pc * mod)) 24 | if pc <= 10: pc = 10 25 | by = int(round(by * mod * random.uniform(0.9, 1.1))) 26 | if by <= 1000: by = 2000 27 | syn = int(round(random.randint(5, 40) * pc / 100)) 28 | synack = int(round(random.randint(30, 90) * syn / 100)) 29 | return [pc, by, syn, synack] 30 | 31 | 32 | def random_vector(old: List[int]) -> List[int]: 33 | if not old: 34 | return random_subvec(old) + random_subvec(old) + random_subvec(old) + random_subvec(old) 35 | return random_subvec(old[:4]) + random_subvec(old[4:8]) + random_subvec(old[8:12]) + random_subvec(old[12:]) 36 | 37 | 38 | team_ids = [r[0] for r in db.session.query(Team.id).all()] 39 | endtime = int(floor(time.time() // 60)) * 60 40 | 41 | TeamTrafficStats.query.delete() 42 | db.session.commit() 43 | 44 | data = {} 45 | for ts in range(endtime - 720 * 60, endtime + 1, 60): 46 | print(ts, '...', endtime) 47 | data = {team_id: random_vector(data.get(team_id, False)) for team_id in team_ids} 48 | TeamTrafficStats.efficient_insert(ts, data) 49 | db.session.commit() 50 | 51 | print('[DONE]') 52 | -------------------------------------------------------------------------------- /sample_files/demoservice.php: -------------------------------------------------------------------------------- 1 | 1% 9 | last 2 versions 10 | Firefox ESR 11 | not dead 12 | not IE 9-11 # For IE 9-11 support, remove 'not'. -------------------------------------------------------------------------------- /scoreboard/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | # Only exists if Bazel was run 8 | /bazel-out 9 | 10 | # dependencies 11 | /node_modules 12 | 13 | # profiling files 14 | chrome-profiler-events*.json 15 | speed-measure-plugin*.json 16 | 17 | # IDEs and editors 18 | /.idea 19 | .project 20 | .classpath 21 | .c9/ 22 | *.launch 23 | .settings/ 24 | *.sublime-workspace 25 | 26 | # IDE - VSCode 27 | .vscode/* 28 | !.vscode/settings.json 29 | !.vscode/tasks.json 30 | !.vscode/launch.json 31 | !.vscode/extensions.json 32 | .history/* 33 | 34 | # misc 35 | /.angular/cache 36 | /.sass-cache 37 | /connect.lock 38 | /coverage 39 | /libpeerconnection.log 40 | npm-debug.log 41 | yarn-error.log 42 | testem.log 43 | /typings 44 | 45 | # System Files 46 | .DS_Store 47 | Thumbs.db 48 | -------------------------------------------------------------------------------- /scoreboard/README.md: -------------------------------------------------------------------------------- 1 | # Scoreboard 2 | 3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 8.3.12. 4 | 5 | ## Development server 6 | 7 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. 8 | 9 | ## Code scaffolding 10 | 11 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. 12 | 13 | ## Build 14 | 15 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build. 16 | 17 | ## Running unit tests 18 | 19 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 20 | 21 | ## Running end-to-end tests 22 | 23 | Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/). 24 | 25 | ## Further help 26 | 27 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md). 28 | -------------------------------------------------------------------------------- /scoreboard/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "scoreboard", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build --configuration production --aot", 8 | "build-gzip": "ng build --configuration production --aot && gzip-all \"dist/scoreboard/*.{css,js,html,svg,woff,woff2,eot,ttf}\"", 9 | "test": "ng test", 10 | "lint": "ng lint", 11 | "e2e": "ng e2e" 12 | }, 13 | "private": true, 14 | "dependencies": { 15 | "@angular/animations": "^18.2.13", 16 | "@angular/cdk": "^18.2.14", 17 | "@angular/common": "^18.2.13", 18 | "@angular/compiler": "^18.2.13", 19 | "@angular/core": "^18.2.13", 20 | "@angular/forms": "^18.2.13", 21 | "@angular/platform-browser": "^18.2.13", 22 | "@angular/platform-browser-dynamic": "^18.2.13", 23 | "@angular/router": "^18.2.13", 24 | "@efaps/ngx-store": "^9.0.0", 25 | "@fortawesome/fontawesome-free": "^5.15.4", 26 | "@kurkle/color": "^0.1.9", 27 | "bootstrap": "^3.4.1", 28 | "chart.js": "^4.4.6", 29 | "lodash": "^4.17.21", 30 | "ng2-charts": "^7.0.0", 31 | "ngx-bootstrap": "^18.1.3", 32 | "rxjs": "~7.8.1", 33 | "ts-debug": "^1.3.0", 34 | "tslib": "^2.0.0", 35 | "zone.js": "~0.14.10" 36 | }, 37 | "devDependencies": { 38 | "@angular/build": "^18.2.12", 39 | "@angular/cli": "^18.2.12", 40 | "@angular/compiler-cli": "^18.2.13", 41 | "@angular/language-service": "^18.2.13", 42 | "@types/node": "^12.11.1", 43 | "@types/lodash": "^4.17.13", 44 | "gzip-all": "^1.0.0", 45 | "less": "^4.2.0", 46 | "ts-node": "~7.0.0", 47 | "tslint": "~6.1.0", 48 | "typescript": "~5.4.5" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /scoreboard/proxy.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "/api/": { 3 | "target": "http://127.0.0.19:8080/", 4 | "secure": false 5 | }, 6 | "/logos/": { 7 | "target": "http://127.0.0.19:8080/", 8 | "secure": false 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /scoreboard/src/.htaccess: -------------------------------------------------------------------------------- 1 | Header add Access-Control-Allow-Origin "*" 2 | Header add Access-Control-Allow-Methods: "GET,POST,OPTIONS,DELETE,PUT" 3 | 4 | FileETag MTime Size 5 | 6 | Header set Cache-Control "max-age=0, public, must-revalidate" 7 | 8 | 9 | Header set Cache-Control "max-age=300, public, must-revalidate" 10 | 11 | -------------------------------------------------------------------------------- /scoreboard/src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {Routes, RouterModule} from '@angular/router'; 3 | import {PageNotFoundComponent} from "./page-not-found/page-not-found.component"; 4 | import {PageIndexComponent} from "./page-index/page-index.component"; 5 | import {PageTeamComponent} from "./page-team/page-team.component"; 6 | import {PageGraphsComponent} from "./page-graphs/page-graphs.component"; 7 | 8 | const routes: Routes = [ 9 | {path: '', component: PageIndexComponent}, 10 | {path: 'team/:teamid', component: PageTeamComponent}, 11 | {path: 'graphs', component: PageGraphsComponent}, 12 | {path: '**', component: PageNotFoundComponent} 13 | ]; 14 | 15 | @NgModule({ 16 | imports: [RouterModule.forRoot(routes, { 17 | initialNavigation: 'enabledBlocking' 18 | })], 19 | exports: [RouterModule] 20 | }) 21 | export class AppRoutingModule { 22 | } 23 | -------------------------------------------------------------------------------- /scoreboard/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |
5 | 6 |
7 |

8 | saarCTF Scoreboard 9 | 10 | 11 | 12 | 13 |

14 | back to scoreboard 16 |
17 |
18 |
19 |
20 |
21 | 22 |
23 |
24 | 29 |
30 | -------------------------------------------------------------------------------- /scoreboard/src/app/app.component.less: -------------------------------------------------------------------------------- 1 | .flexible-container { 2 | display: inline-block; 3 | } 4 | 5 | .header-inner { 6 | position: sticky; 7 | left: 0; 8 | width: 100%; 9 | max-width: 100vw; 10 | } 11 | 12 | a.backlink { 13 | color: white; 14 | } 15 | 16 | .credits { 17 | text-align: center; 18 | margin-top: 20px; 19 | margin-bottom: 20px; 20 | width: 100%; 21 | max-width: 100vw; 22 | position: sticky; 23 | left: 0; 24 | } 25 | -------------------------------------------------------------------------------- /scoreboard/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | import {setTheme} from "ngx-bootstrap/utils"; 3 | 4 | @Component({ 5 | selector: 'app-root', 6 | templateUrl: 'app.component.html', 7 | styleUrls: ['app.component.less'] 8 | }) 9 | export class AppComponent { 10 | title = 'scoreboard'; 11 | 12 | constructor() { 13 | setTheme('bs4'); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /scoreboard/src/app/backend.service.spec.ts: -------------------------------------------------------------------------------- 1 | import {TestBed} from '@angular/core/testing'; 2 | 3 | import {BackendService} from './backend.service'; 4 | 5 | describe('BackendService', () => { 6 | beforeEach(() => TestBed.configureTestingModule({})); 7 | 8 | it('should be created', () => { 9 | const service: BackendService = TestBed.get(BackendService); 10 | expect(service).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /scoreboard/src/app/current-tick/current-tick.component.html: -------------------------------------------------------------------------------- 1 |
2 |

Tick {{backend.currentState.current_tick}}

3 |

4 |   5 | 6 | {{formatRemainingTime(currentTime)}} 7 | 8 | game suspended 9 | game is over 10 | not started 11 |

12 |
13 | -------------------------------------------------------------------------------- /scoreboard/src/app/current-tick/current-tick.component.less: -------------------------------------------------------------------------------- 1 | .current-tick { 2 | display: inline-block; 3 | 4 | .tick { 5 | margin-top: 9px; 6 | font-size: 24px; 7 | text-align: right; 8 | } 9 | 10 | .time { 11 | text-align: right; 12 | font-size: 18px; 13 | margin-top: 15px; 14 | } 15 | 16 | .time span { 17 | margin-left: 5px; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /scoreboard/src/app/current-tick/current-tick.component.spec.ts: -------------------------------------------------------------------------------- 1 | import {async, ComponentFixture, TestBed} from '@angular/core/testing'; 2 | 3 | import {CurrentTickComponent} from './current-tick.component'; 4 | 5 | describe('CurrentTickComponent', () => { 6 | let component: CurrentTickComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [CurrentTickComponent] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(CurrentTickComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /scoreboard/src/app/current-tick/current-tick.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnDestroy, OnInit} from '@angular/core'; 2 | import {BackendService, GameStates} from "../backend.service"; 3 | 4 | @Component({ 5 | selector: 'app-current-tick', 6 | templateUrl: './current-tick.component.html', 7 | styleUrls: ['./current-tick.component.less'] 8 | }) 9 | export class CurrentTickComponent implements OnInit, OnDestroy { 10 | 11 | public currentTime = 0; 12 | public currentTimeInterval; 13 | public GameStates = GameStates; 14 | 15 | constructor(public backend: BackendService) { 16 | } 17 | 18 | ngOnInit() { 19 | this.currentTimeInterval = setInterval(() => this.currentTime = Math.round(new Date().getTime() / 1000), 500); 20 | } 21 | 22 | ngOnDestroy() { 23 | clearInterval(this.currentTimeInterval); 24 | } 25 | 26 | formatRemainingTime(currentTime: number) { 27 | // server time offset! 28 | let remaining = this.backend.currentState.current_tick_until - currentTime + this.backend.deltaClientToServer; 29 | if (remaining < 0) remaining = 0; 30 | let m = Math.floor(remaining / 60); 31 | let s = Math.floor(remaining % 60); 32 | return (m < 10 ? '0' + m : m) + ':' + (s < 10 ? '0' + s : s); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /scoreboard/src/app/notification-overlay/notification-overlay.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | First Blood 5 |
6 | 7 |
8 |
9 |
on {{servicename}}
10 |
by {{teamname}}
11 |
12 | 13 |
14 |
15 |
16 |
17 |
18 | 19 |
20 | Game is over! 21 |
22 | 23 |
24 | 25 | 26 | 27 | 30 | 31 | 32 | 33 |
{{i + 1}}. 28 | 29 | {{teaminfo[0].name}}{{teaminfo[1].toFixed(1)}} points
34 |
35 | 36 |
37 | Thanks for playing!
38 | this game was powered by saarsec 39 |
40 |
41 | -------------------------------------------------------------------------------- /scoreboard/src/app/notification-overlay/notification-overlay.component.spec.ts: -------------------------------------------------------------------------------- 1 | import {async, ComponentFixture, TestBed} from '@angular/core/testing'; 2 | 3 | import {NotificationOverlayComponent} from './notification-overlay.component'; 4 | 5 | describe('NotificationOverlayComponent', () => { 6 | let component: NotificationOverlayComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [NotificationOverlayComponent] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(NotificationOverlayComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /scoreboard/src/app/page-graphs/page-graphs.component.html: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 |
6 |

{{ chart.title }}

7 |
8 | 12 | 13 |
14 |
15 | 16 |
17 |

{{ chart.title }}

18 |
19 | 23 | 24 |
25 |
-------------------------------------------------------------------------------- /scoreboard/src/app/page-graphs/page-graphs.component.less: -------------------------------------------------------------------------------- 1 | .chart-container { 2 | // position: sticky; 3 | left: 15px; 4 | height: 420px; 5 | width: calc(100vw - 28px); 6 | } 7 | 8 | .chart-group { 9 | margin-bottom: 4rem; 10 | } -------------------------------------------------------------------------------- /scoreboard/src/app/page-graphs/page-graphs.component.spec.ts: -------------------------------------------------------------------------------- 1 | import {ComponentFixture, TestBed} from '@angular/core/testing'; 2 | 3 | import {PageGraphsComponent} from './page-graphs.component'; 4 | 5 | describe('PageGraphsComponent', () => { 6 | let component: PageGraphsComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [PageGraphsComponent] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(PageGraphsComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /scoreboard/src/app/page-index/page-index.component.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /scoreboard/src/app/page-index/page-index.component.less: -------------------------------------------------------------------------------- 1 | tablelinecells { 2 | display: none; 3 | } -------------------------------------------------------------------------------- /scoreboard/src/app/page-index/page-index.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { PageIndexComponent } from './page-index.component'; 4 | 5 | describe('PageIndexComponent', () => { 6 | let component: PageIndexComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ PageIndexComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(PageIndexComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /scoreboard/src/app/page-index/page-index.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-page-index', 5 | templateUrl: './page-index.component.html', 6 | styleUrls: ['./page-index.component.less'] 7 | }) 8 | export class PageIndexComponent implements OnInit { 9 | 10 | constructor() { 11 | } 12 | 13 | ngOnInit(): void { 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /scoreboard/src/app/page-not-found/page-not-found.component.html: -------------------------------------------------------------------------------- 1 |
Page not found
-------------------------------------------------------------------------------- /scoreboard/src/app/page-not-found/page-not-found.component.less: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarkusBauer/saarctf-gameserver/88bee31ca3bf56492714eac5d0c6f96bc1a92079/scoreboard/src/app/page-not-found/page-not-found.component.less -------------------------------------------------------------------------------- /scoreboard/src/app/page-not-found/page-not-found.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { PageNotFoundComponent } from './page-not-found.component'; 4 | 5 | describe('PageNotFoundComponent', () => { 6 | let component: PageNotFoundComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ PageNotFoundComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(PageNotFoundComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /scoreboard/src/app/page-not-found/page-not-found.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-page-not-found', 5 | templateUrl: './page-not-found.component.html', 6 | styleUrls: ['./page-not-found.component.less'] 7 | }) 8 | export class PageNotFoundComponent implements OnInit { 9 | 10 | constructor() { 11 | } 12 | 13 | ngOnInit(): void { 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /scoreboard/src/app/page-team/page-team.component.less: -------------------------------------------------------------------------------- 1 | td, th { 2 | vertical-align: middle; 3 | } 4 | 5 | p { 6 | margin: 5px 0; 7 | } 8 | 9 | .headercell { 10 | .fa-spin { 11 | margin-left: 10px; 12 | } 13 | } 14 | 15 | .rankcell { 16 | //padding-right: 0; 17 | border-right: none; 18 | } 19 | 20 | .teamcell { 21 | border-left: none; 22 | } 23 | 24 | .team-logo-media { 25 | padding-right: 15px; 26 | 27 | > img { 28 | width: 64px; 29 | max-width: 64px; 30 | max-height: 64px; 31 | } 32 | } 33 | 34 | .team-name { 35 | margin-top: 3px; 36 | margin-bottom: 1px; 37 | } 38 | 39 | .team-vulnbox { 40 | font-size: 75%; 41 | } 42 | 43 | .team-total-points { 44 | font-size: 16px; 45 | white-space: nowrap; 46 | } 47 | 48 | tablelinecells, app-table-service-header-cell { 49 | display: none; 50 | } 51 | 52 | .chart-container { 53 | position: sticky; 54 | left: 15px; 55 | height: 280px; 56 | width: calc(100vw - 28px); 57 | } 58 | 59 | .fa-spinner[hidden] { 60 | display: none 61 | } -------------------------------------------------------------------------------- /scoreboard/src/app/page-team/page-team.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { PageTeamComponent } from './page-team.component'; 4 | 5 | describe('PageTeamComponent', () => { 6 | let component: PageTeamComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ PageTeamComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(PageTeamComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /scoreboard/src/app/ratelimiter.ts: -------------------------------------------------------------------------------- 1 | import {asyncScheduler, Observable, of, SchedulerLike} from "rxjs"; 2 | import {concatAll, delay, map} from "rxjs/operators"; 3 | 4 | /** 5 | * Limit the value stream of an Observable, ensures at least "delay" milliseconds between two values. 6 | */ 7 | export class RateLimiter { 8 | 9 | private lastValueIssued = 0; 10 | 11 | constructor(private delay: number, private scheduler: SchedulerLike = asyncScheduler) { 12 | } 13 | 14 | limit(stream: Observable): Observable { 15 | return stream.pipe( 16 | map(x => { 17 | const now = this.scheduler.now(); 18 | const wait = this.lastValueIssued + this.delay - now; 19 | if (wait > 0) { 20 | this.lastValueIssued += this.delay; 21 | return of(x).pipe(delay(Math.min(wait, this.delay), this.scheduler)); 22 | } else { 23 | this.lastValueIssued = now; 24 | return [x]; 25 | } 26 | }), 27 | concatAll() 28 | ); 29 | } 30 | 31 | } -------------------------------------------------------------------------------- /scoreboard/src/app/retryWithBackoff.ts: -------------------------------------------------------------------------------- 1 | import {Observable, of, throwError} from "rxjs"; 2 | import {delay, mergeMap, retryWhen} from "rxjs/operators"; 3 | 4 | export function retryWithBackoff(delayMs: number, maxRetry = 5, backoffMs = 1500) { 5 | let retries = maxRetry; 6 | return (src: Observable) => src.pipe( 7 | retryWhen((errors: Observable) => errors.pipe( 8 | mergeMap(error => { 9 | if (retries-- > 0) { 10 | const backoffTime = delayMs + (maxRetry - retries) * backoffMs; 11 | return of(error).pipe(delay(backoffTime)); 12 | } 13 | return throwError(`HTTP request failed after ${maxRetry} attempts.`); 14 | }) 15 | )) 16 | ); 17 | } -------------------------------------------------------------------------------- /scoreboard/src/app/scoretable/scoretable.component.less: -------------------------------------------------------------------------------- 1 | @import "../../variables.less"; 2 | 3 | table { 4 | td, th { 5 | vertical-align: middle; 6 | } 7 | 8 | p { 9 | margin: 5px 0; 10 | } 11 | } 12 | 13 | .headercell { 14 | .fa-spin { 15 | margin-left: 10px; 16 | } 17 | } 18 | 19 | .rankcell { 20 | padding-right: 0; 21 | border-right: none; 22 | } 23 | 24 | .teamcell { 25 | border-left: none; 26 | } 27 | 28 | .team-logo-media { 29 | padding-right: 15px; 30 | 31 | > img, > a > img { 32 | width: 64px; 33 | max-width: 64px; 34 | max-height: 64px; 35 | } 36 | } 37 | 38 | .team-name { 39 | margin-top: 3px; 40 | margin-bottom: 1px; 41 | display: flex; 42 | max-width: 200px; 43 | 44 | a { 45 | color: @text-color; 46 | flex: 1 1 auto; 47 | overflow: hidden; 48 | text-overflow: ellipsis; 49 | } 50 | 51 | &.blocked { 52 | a { 53 | color: @state-danger-text; 54 | text-decoration: line-through; 55 | } 56 | } 57 | } 58 | :host-context(html.dark) .team-name a { 59 | color: @dm-text-color; 60 | } 61 | 62 | :host-context(html.dark) .team-name.blocked a { 63 | color: @dm-danger-color; 64 | } 65 | 66 | .team-vulnbox { 67 | font-size: 75%; 68 | } 69 | 70 | .team-total-points { 71 | font-size: 16px; 72 | } 73 | 74 | tablelinecells, app-table-service-header-cell { 75 | display: none; 76 | } -------------------------------------------------------------------------------- /scoreboard/src/app/scoretable/scoretable.component.spec.ts: -------------------------------------------------------------------------------- 1 | import {async, ComponentFixture, TestBed} from '@angular/core/testing'; 2 | 3 | import {ScoretableComponent} from './scoretable.component'; 4 | 5 | describe('ScoretableComponent', () => { 6 | let component: ScoretableComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ScoretableComponent] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ScoretableComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /scoreboard/src/app/settings/settings.component.less: -------------------------------------------------------------------------------- 1 | @import "../../variables"; 2 | 3 | :host-context(html.dark) #settings-button { 4 | color: desaturate(lighten(@brand-primary, 20%), 20%); 5 | 6 | &:hover, 7 | &:focus { 8 | color: lighten(desaturate(lighten(@brand-primary, 20%), 20%), 15%); 9 | } 10 | } -------------------------------------------------------------------------------- /scoreboard/src/app/settings/settings.component.spec.ts: -------------------------------------------------------------------------------- 1 | import {async, ComponentFixture, TestBed} from '@angular/core/testing'; 2 | 3 | import {SettingsComponent} from './settings.component'; 4 | 5 | describe('SettingsComponent', () => { 6 | let component: SettingsComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [SettingsComponent] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(SettingsComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /scoreboard/src/app/settings/settings.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from '@angular/core'; 2 | import {BsDropdownConfig} from "ngx-bootstrap/dropdown"; 3 | import {UiService} from "../ui.service"; 4 | 5 | @Component({ 6 | selector: 'app-settings', 7 | templateUrl: './settings.component.html', 8 | styleUrls: ['./settings.component.less'], 9 | providers: [{provide: BsDropdownConfig, useValue: {isAnimated: false, autoClose: false}}] 10 | }) 11 | export class SettingsComponent implements OnInit { 12 | 13 | constructor(public ui: UiService) { 14 | } 15 | 16 | ngOnInit() { 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /scoreboard/src/app/table-line-cells/table-line-cells.component.less: -------------------------------------------------------------------------------- 1 | @import "../../variables.less"; 2 | 3 | td, th { 4 | vertical-align: middle; 5 | } 6 | 7 | p { 8 | margin: 5px 0; 9 | } 10 | 11 | @result-grid-gap: 3px; 12 | @result-grid-gap-large: 12px; 13 | 14 | .sum-container, .result-container { 15 | display: grid; 16 | grid-template-columns: 33% 33% 33%; 17 | grid-column-gap: @result-grid-gap; 18 | 19 | // Icon color 20 | > i.fas { 21 | color: #222; 22 | align-self: center; 23 | } 24 | 25 | > .points, > .delta { 26 | justify-self: end; 27 | } 28 | } 29 | 30 | .sum-container { 31 | grid-template-columns: @line-height-computed max-content; 32 | } 33 | 34 | .result-container { 35 | grid-template-columns: @line-height-computed max-content max-content calc(@line-height-computed + @result-grid-gap-large) max-content max-content; 36 | 37 | > .offset-left { 38 | margin-left: @result-grid-gap-large; 39 | } 40 | 41 | > .delta { 42 | align-self: end; 43 | } 44 | 45 | > .sla-col { 46 | grid-column: 4 / span 3; 47 | vertical-align: middle; 48 | white-space: nowrap; 49 | 50 | .status.label { 51 | padding: 0.3em 0.6em; 52 | border-radius: 0.35em; 53 | display: inline-block; 54 | vertical-align: middle; 55 | user-select: none; 56 | } 57 | 58 | .info-link { 59 | margin-left: 5px; 60 | float: right; 61 | } 62 | 63 | .history-3, .history-2, .history-1 { 64 | display: inline-block; 65 | border-radius: 50%; 66 | margin-right: 4px; 67 | padding: 0; 68 | vertical-align: middle; 69 | 70 | &.history-3 { 71 | width: 8px; 72 | height: 8px; 73 | opacity: 0.7; 74 | } 75 | 76 | &.history-2 { 77 | width: 10px; 78 | height: 10px; 79 | opacity: 0.8; 80 | } 81 | 82 | &.history-1 { 83 | width: 12px; 84 | height: 12px; 85 | opacity: 0.9; 86 | margin-right: 6px; 87 | } 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /scoreboard/src/app/table-line-cells/table-line-cells.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { TableLineCellsComponent } from './table-line-cells.component'; 4 | 5 | describe('TableLineCellsComponent', () => { 6 | let component: TableLineCellsComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ TableLineCellsComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(TableLineCellsComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /scoreboard/src/app/table-service-header-cell/table-service-header-cell.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

{{service.name}}

4 |

5 | {{service.attackers + (service.attackers != 1 ? ' attackers' : ' attacker')}}
6 | {{service.victims + (service.victims != 1 ? ' victims' : ' victim')}} 7 |
{{service.first_blood.join(', ')}}
9 | 11 |
12 | {{ service.flag_stores - service.flag_stores_exploited }} 13 | flag stores 14 | unexploited 15 |
16 |

17 | 18 |
-------------------------------------------------------------------------------- /scoreboard/src/app/table-service-header-cell/table-service-header-cell.component.less: -------------------------------------------------------------------------------- 1 | @import "../../variables.less"; 2 | 3 | p { 4 | margin: 5px 0; 5 | } 6 | 7 | .servicecell { 8 | text-align: center; 9 | 10 | .attacker-victim-count { 11 | display: inline-block; 12 | text-align: left; 13 | 14 | .fa-tint { 15 | padding-left: 1px; 16 | padding-right: 1px; 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /scoreboard/src/app/table-service-header-cell/table-service-header-cell.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { TableServiceHeaderCellComponent } from './table-service-header-cell.component'; 4 | 5 | describe('TableServiceHeaderCellComponent', () => { 6 | let component: TableServiceHeaderCellComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ TableServiceHeaderCellComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(TableServiceHeaderCellComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /scoreboard/src/app/table-service-header-cell/table-service-header-cell.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, Input, OnInit, ViewChild, ViewContainerRef} from '@angular/core'; 2 | import {Service} from "../models"; 3 | 4 | @Component({ 5 | selector: 'app-table-service-header-cell', 6 | templateUrl: './table-service-header-cell.component.html', 7 | styleUrls: ['./table-service-header-cell.component.less'] 8 | }) 9 | export class TableServiceHeaderCellComponent implements OnInit { 10 | 11 | @Input() services: Array; 12 | 13 | @ViewChild('template', {static: true}) template; 14 | 15 | constructor(private viewContainerRef: ViewContainerRef) { 16 | } 17 | 18 | ngOnInit(): void { 19 | this.viewContainerRef.createEmbeddedView(this.template); 20 | } 21 | 22 | indexTrack(index, item) { 23 | return index; 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /scoreboard/src/app/ui.service.spec.ts: -------------------------------------------------------------------------------- 1 | import {TestBed} from '@angular/core/testing'; 2 | 3 | import {UiService} from './ui.service'; 4 | 5 | describe('UiService', () => { 6 | beforeEach(() => TestBed.configureTestingModule({})); 7 | 8 | it('should be created', () => { 9 | const service: UiService = TestBed.get(UiService); 10 | expect(service).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /scoreboard/src/app/ui.service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import {Subject} from "rxjs"; 3 | import {setSchemeDarkmode} from "./chart-colorschemes"; 4 | import {SessionStorage} from "@efaps/ngx-store"; 5 | 6 | /** 7 | * Service storing user preferences (in session storage for persistence). 8 | */ 9 | @Injectable({ 10 | providedIn: 'root' 11 | }) 12 | export class UiService { 13 | 14 | @SessionStorage({key: 'showHistory'}) 15 | public showHistory: boolean = true; 16 | @SessionStorage({key: 'showOnlySums'}) 17 | public showOnlySums: boolean = false; 18 | @SessionStorage({key: 'showImages'}) 19 | public showImages: boolean = true; 20 | @SessionStorage({key: 'showNotifications'}) 21 | public showNotifications: boolean = true; 22 | @SessionStorage({key: 'darkmode'}) 23 | public darkmode: boolean = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; 24 | public darkmodeChanges = new Subject(); 25 | 26 | constructor() { 27 | this.setDarkmode(this.darkmode); 28 | } 29 | 30 | setDarkmode(enabled: boolean) { 31 | if (enabled) { 32 | document.body.parentElement.classList.add('dark'); 33 | } else { 34 | document.body.parentElement.classList.remove('dark'); 35 | } 36 | setSchemeDarkmode(enabled); 37 | if (enabled != this.darkmode) { 38 | this.darkmodeChanges.next(enabled); 39 | } 40 | this.darkmode = enabled; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /scoreboard/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarkusBauer/saarctf-gameserver/88bee31ca3bf56492714eac5d0c6f96bc1a92079/scoreboard/src/assets/.gitkeep -------------------------------------------------------------------------------- /scoreboard/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /scoreboard/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false 7 | }; 8 | 9 | /* 10 | * For easier debugging in development mode, you can import the following file 11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 12 | * 13 | * This import should be commented out in production mode because it will have a negative impact 14 | * on performance if an error is thrown. 15 | */ 16 | // import 'zone.js/plugins/zone-error'; // Included with Angular CLI. 17 | -------------------------------------------------------------------------------- /scoreboard/src/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarkusBauer/saarctf-gameserver/88bee31ca3bf56492714eac5d0c6f96bc1a92079/scoreboard/src/favicon.png -------------------------------------------------------------------------------- /scoreboard/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | saarCTF Scoreboard 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /scoreboard/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.error(err)); 13 | -------------------------------------------------------------------------------- /scoreboard/src/variables.less: -------------------------------------------------------------------------------- 1 | @import "bootstrap/less/variables.less"; 2 | @import "../../controlserver/static/less/color.less"; 3 | 4 | @font-size-base: 13px; 5 | 6 | @table-head-bg-color: #fff; 7 | 8 | @dm-text-color: darken(#fff, 15%); 9 | @dm-icon-color: darken(#fff, 40%); 10 | @dm-body-bg: #121212; 11 | @dm-link-color: lighten(#1E91D6, 10%); // desaturate(lighten(@brand-primary, 30%), 10%); 12 | @dm-warning-color: #FF9F1C; 13 | @dm-danger-color: saturate(lighten(@brand-danger, 10%), 30%); 14 | @dm-table-border-color: @gray; 15 | @dm-table-head-bg-color: lighten(@dm-body-bg, 7%); 16 | @dm-table-elevated-col-bg-color: lighten(@dm-body-bg, 4.5%); 17 | @dm-popover-bg: lighten(@dm-body-bg, 5%); 18 | @dm-popover-title-bg: lighten(@dm-body-bg, 10%); 19 | @dm-popover-border-color: @gray; 20 | @dm-popover-arrow-outer-color: fadein(@dm-popover-border-color, 5%); 21 | @dm-popover-arrow-color: @dm-popover-border-color; 22 | @dm-btn-default-bg: lighten(@gray-base, 20%); // original "@gray-dark" 23 | -------------------------------------------------------------------------------- /scoreboard/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/app", 5 | "types": [] 6 | }, 7 | "angularCompilerOptions": { 8 | }, 9 | "files": [ 10 | "src/main.ts" 11 | ], 12 | "include": [ 13 | "src/**/*.d.ts" 14 | ], 15 | "exclude": [ 16 | "src/test.ts", 17 | "src/**/*.spec.ts" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /scoreboard/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "esModuleInterop": true, 8 | "declaration": false, 9 | "experimentalDecorators": true, 10 | "module": "es2020", 11 | "moduleResolution": "node", 12 | "importHelpers": true, 13 | "target": "ES2022", 14 | "typeRoots": [ 15 | "node_modules/@types" 16 | ], 17 | "lib": [ 18 | "es2018", 19 | "dom" 20 | ], 21 | "paths": { 22 | "chart.js/dist/types/utils": ["node_modules/chart.js/dist/types/utils.d.ts"] 23 | }, 24 | "useDefineForClassFields": false 25 | }, 26 | "angularCompilerOptions": { 27 | "fullTemplateTypeCheck": true, 28 | "strictInjectionParameters": true 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /scripts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarkusBauer/saarctf-gameserver/88bee31ca3bf56492714eac5d0c6f96bc1a92079/scripts/__init__.py -------------------------------------------------------------------------------- /scripts/export_ctftime_scoreboard.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from typing import Optional 4 | 5 | sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 6 | 7 | from controlserver.timer import init_slave_timer, CTFState 8 | from controlserver.models import init_database 9 | from saarctf_commons.redis import NamedRedisConnection 10 | from saarctf_commons.config import config, load_default_config 11 | from controlserver.scoring.scoreboard import Scoreboard 12 | from controlserver.scoring.scoring import ScoringCalculation 13 | 14 | """ 15 | ARGUMENTS: filename (default: stdout) 16 | """ 17 | 18 | 19 | def export_ctftime_scoreboard(fname: str | None) -> None: 20 | init_database() 21 | init_slave_timer() 22 | from controlserver.timer import Timer 23 | scoring = ScoringCalculation(config.SCORING) 24 | scoreboard = Scoreboard(scoring) 25 | tick = Timer.current_tick if Timer.state != CTFState.RUNNING else Timer.current_tick - 1 26 | data = scoreboard.create_ctftime_json(tick) 27 | if fname: 28 | fname = os.path.abspath(fname) 29 | with open(fname, 'w') as f: 30 | f.write(data) 31 | print(f'Saved scoreboard to "{fname}".') 32 | else: 33 | print(data) 34 | 35 | 36 | if __name__ == '__main__': 37 | load_default_config() 38 | config.set_script() 39 | NamedRedisConnection.set_clientname('script-' + os.path.basename(__file__)) 40 | export_ctftime_scoreboard(sys.argv[1] if len(sys.argv) > 1 else None) 41 | -------------------------------------------------------------------------------- /scripts/manual_dispatch-collect.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import time 4 | 5 | from checker_runner.runner import celery_worker 6 | from controlserver.models import init_database 7 | from saarctf_commons.config import config, load_default_config 8 | from controlserver.dispatcher import Dispatcher 9 | from saarctf_commons.redis import NamedRedisConnection 10 | 11 | """ 12 | ARGUMENTS: tick (optional) 13 | """ 14 | 15 | if __name__ == '__main__': 16 | load_default_config() 17 | config.set_script() 18 | NamedRedisConnection.set_clientname('script-' + os.path.basename(__file__)) 19 | celery_worker.init() 20 | 21 | if len(sys.argv) <= 1: 22 | from controlserver.timer import Timer, init_slave_timer 23 | init_slave_timer() 24 | 25 | tick = Timer.current_tick 26 | else: 27 | tick = int(sys.argv[1]) 28 | 29 | init_database() 30 | 31 | t = time.time() 32 | dispatcher = Dispatcher() 33 | dispatcher.collect_checker_results(tick) 34 | print('Collected checker scripts for tick {}. Took {:.1f} sec'.format(tick, time.time() - t)) 35 | -------------------------------------------------------------------------------- /scripts/manual_dispatch.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import time 4 | 5 | from checker_runner.runner import celery_worker 6 | from controlserver.models import init_database 7 | from saarctf_commons.config import config, load_default_config 8 | from saarctf_commons.redis import NamedRedisConnection 9 | from controlserver.dispatcher import Dispatcher 10 | 11 | """ 12 | ARGUMENTS: tick (optional) 13 | """ 14 | 15 | if __name__ == '__main__': 16 | load_default_config() 17 | config.set_script() 18 | NamedRedisConnection.set_clientname('script-' + os.path.basename(__file__)) 19 | celery_worker.init() 20 | 21 | # config.set_redis_clientname(os.path.basename(__file__)) 22 | if len(sys.argv) <= 1: 23 | from controlserver.timer import Timer, init_slave_timer 24 | init_slave_timer() 25 | 26 | tick = Timer.current_tick 27 | else: 28 | tick = int(sys.argv[1]) 29 | 30 | init_database() 31 | 32 | t = time.time() 33 | dispatcher = Dispatcher() 34 | dispatcher.dispatch_checker_scripts(tick) 35 | print('Checker scripts for tick {} dispatched. Took {:.1f} sec'.format(tick, time.time() - t)) 36 | -------------------------------------------------------------------------------- /scripts/patch_prepare.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from pathlib import Path 4 | 5 | sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 6 | 7 | from controlserver.models import init_database 8 | from controlserver.patch_utils import PatchUtils 9 | from saarctf_commons.config import config, load_default_config 10 | from saarctf_commons.redis import NamedRedisConnection 11 | 12 | """ 13 | ARGUMENTS: [ ...] 14 | """ 15 | 16 | if __name__ == '__main__': 17 | load_default_config() 18 | config.set_script() 19 | NamedRedisConnection.set_clientname('script-' + os.path.basename(__file__)) 20 | init_database() 21 | # create ansible files 22 | p = PatchUtils(sys.argv[1], [Path(s) for s in sys.argv[2:]]) 23 | tmpl = p.create_ansible_template() 24 | print('[*] Created ansible template:', tmpl) 25 | tmpl = p.create_ansible_hosts_template() 26 | print('[*] Created hosts template: ', tmpl) 27 | print(' Use patch_publish.py when you\'re done editing.') 28 | -------------------------------------------------------------------------------- /scripts/patch_publish.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from pathlib import Path 4 | 5 | sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 6 | 7 | from controlserver.models import init_database 8 | from controlserver.patch_utils import PatchUtils 9 | from saarctf_commons.redis import NamedRedisConnection 10 | from saarctf_commons.config import config, load_default_config 11 | 12 | """ 13 | ARGUMENTS: [ ...] 14 | """ 15 | 16 | if __name__ == '__main__': 17 | load_default_config() 18 | config.set_script() 19 | NamedRedisConnection.set_clientname('script-' + os.path.basename(__file__)) 20 | init_database() 21 | # create ansible files 22 | p = PatchUtils(sys.argv[1], [Path(s) for s in sys.argv[2:]]) 23 | tmpl = p.create_ansible_hosts_template() 24 | print('[*] Created hosts template: ', tmpl) 25 | urls = p.publish_patch_files() 26 | print('[*] Published patch files. Download URLs:') 27 | for url in urls: 28 | print(f'- {url}') 29 | print('') 30 | # ansible guidelines 31 | print('To test this patch against NOP team:') 32 | print(f'> cd "{os.path.dirname(p.ansible_filename())}"') 33 | print(f'> ansible-playbook {os.path.basename(p.ansible_filename())} -i hosts_nop.yaml') 34 | print('') 35 | print('To deploy this patch:') 36 | print(f'> cd "{os.path.dirname(p.ansible_filename())}"') 37 | print(f'> ansible-playbook {os.path.basename(p.ansible_filename())} -i hosts.yaml') 38 | -------------------------------------------------------------------------------- /scripts/recreate_firstblood.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 5 | 6 | from controlserver.models import init_database 7 | from saarctf_commons.redis import NamedRedisConnection 8 | from saarctf_commons.config import config, load_default_config 9 | from controlserver.scoring.scoring import ScoringCalculation 10 | from saarctf_commons.debug_sql_timing import timing, print_query_stats 11 | 12 | """ 13 | ARGUMENTS: none 14 | """ 15 | 16 | if __name__ == '__main__': 17 | load_default_config() 18 | config.set_script() 19 | NamedRedisConnection.set_clientname('script-' + os.path.basename(__file__)) 20 | init_database() 21 | timing() 22 | scoring = ScoringCalculation(config.SCORING) 23 | print('Recomputing first blood flags now, might take some time ...') 24 | scoring.recompute_first_blood_flags() 25 | timing('First Blood') 26 | print('Done.') 27 | if '--stats' in sys.argv: 28 | print_query_stats() 29 | -------------------------------------------------------------------------------- /scripts/service_update.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | 5 | sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 6 | 7 | from saarctf_commons.config import config, load_default_config 8 | from controlserver.models import init_database 9 | from controlserver.service_mgr import ServiceRepoManager 10 | 11 | """ 12 | Clone/update service repositories and update metadata in database. 13 | Does not update existing checker scripts, tries to preserve manual changes. 14 | ARGUMENTS: none 15 | """ 16 | 17 | if __name__ == '__main__': 18 | load_default_config() 19 | config.set_script() 20 | init_database() 21 | 22 | mgr = ServiceRepoManager() 23 | mgr.update_all_services() 24 | print('[OK] Service update complete') 25 | print(' You might want to update the checker scripts in UI.') 26 | -------------------------------------------------------------------------------- /scripts/test_dispatchspeed.py: -------------------------------------------------------------------------------- 1 | from checker_runner.runner import * 2 | from saarctf_commons.debug_sql_timing import timing, print_query_stats 3 | 4 | 5 | def main(): 6 | from controlserver.dispatcher import Dispatcher 7 | dispatcher = Dispatcher() 8 | 9 | for rn in range(505, 507): 10 | timing() 11 | dispatcher.dispatch_checker_scripts(rn) 12 | timing('dispatch') 13 | 14 | time.sleep(2) 15 | 16 | timing() 17 | # print(dispatcher.get_round_taskgroup(rn).get()) 18 | dispatcher.collect_checker_results(rn) 19 | timing('collect') 20 | 21 | print_query_stats() 22 | 23 | 24 | if __name__ == '__main__': 25 | main() 26 | -------------------------------------------------------------------------------- /scripts/worker_pool_status.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | 4 | sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 5 | 6 | from saarctf_commons.config import load_default_config, config 7 | from saarctf_commons.redis import NamedRedisConnection 8 | from scripts.worker_pool_increase import FlowerInterface, print_workers 9 | 10 | 11 | def main() -> None: 12 | flower = FlowerInterface() 13 | workers = flower.get_worker_pool_size() 14 | online = flower.get_worker_online() 15 | print_workers(workers, online) 16 | 17 | 18 | if __name__ == '__main__': 19 | load_default_config() 20 | config.set_script() 21 | NamedRedisConnection.set_clientname('script-' + os.path.basename(__file__)) 22 | main() 23 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | os.environ['SAARCTF_CONFIG'] = basedir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + '/config.test.json' -------------------------------------------------------------------------------- /vpn/.gitignore: -------------------------------------------------------------------------------- 1 | /config-client/ 2 | /config-server/ 3 | /secrets/ 4 | vpncloud.service 5 | -------------------------------------------------------------------------------- /vpn/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarkusBauer/saarctf-gameserver/88bee31ca3bf56492714eac5d0c6f96bc1a92079/vpn/__init__.py -------------------------------------------------------------------------------- /vpn/bpf/.gitignore: -------------------------------------------------------------------------------- 1 | traffic_stats_team*.o -------------------------------------------------------------------------------- /vpn/bpf/Makefile: -------------------------------------------------------------------------------- 1 | CFLAGS=-O2 -I/usr/include/x86_64-linux-gnu 2 | ifeq ($(CC),) 3 | CC=cc 4 | endif 5 | 6 | # find the newest clang compiler 7 | ifeq ($(CC),cc) 8 | ifneq ($(shell clang --version 2>/dev/null),) 9 | CC := clang 10 | else ifneq ($(shell clang-21 --version 2>/dev/null),) 11 | CC := clang-21 12 | else ifneq ($(shell clang-20 --version 2>/dev/null),) 13 | CC := clang-20 14 | else ifneq ($(shell clang-19 --version 2>/dev/null),) 15 | CC := clang-19 16 | else ifneq ($(shell clang-18 --version 2>/dev/null),) 17 | CC := clang-18 18 | else ifneq ($(shell clang-17 --version 2>/dev/null),) 19 | CC := clang-17 20 | else ifneq ($(shell clang-16 --version 2>/dev/null),) 21 | CC := clang-16 22 | else ifneq ($(shell clang-15 --version 2>/dev/null),) 23 | CC := clang-15 24 | else ifneq ($(shell clang-14 --version 2>/dev/null),) 25 | CC := clang-14 26 | else ifneq ($(shell clang-13 --version 2>/dev/null),) 27 | CC := clang-13 28 | else ifneq ($(shell clang-12 --version 2>/dev/null),) 29 | CC := clang-12 30 | else ifneq ($(shell clang-11 --version 2>/dev/null),) 31 | CC := clang-11 32 | endif 33 | endif 34 | 35 | all: 36 | @echo "Using ${CC} as compiler ..." 37 | ${CC} -target bpf -c ${CFLAGS} anonymize_traffic.c -o anonymize_traffic.o 38 | ${CC} -target bpf -c ${CFLAGS} -g traffic_stats.c -o traffic_stats.o 39 | ${CC} -target bpf -c ${CFLAGS} -g traffic_stats_gameserver.c -o traffic_stats_gameserver.o 40 | 41 | vmlinux.h: 42 | @bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h 43 | -------------------------------------------------------------------------------- /vpn/bpf/anonymize_traffic.c: -------------------------------------------------------------------------------- 1 | #include "bpf-utils.h" 2 | #include 3 | #include 4 | #include 5 | 6 | 7 | 8 | #define SAARSEC_MAX_TTL 48 9 | 10 | 11 | static inline int anonymize_frame(struct __sk_buff *skb, const __u64 ipoffset) { 12 | // Has IP packet? 13 | if (skb->data + ipoffset + sizeof(struct iphdr) > skb->data_end) { 14 | return TC_ACT_UNSPEC; 15 | } 16 | 17 | // Limit TTL 18 | struct iphdr *ip = (void*) (skb->data + ipoffset); 19 | __u32 protocol = ip->protocol; 20 | if (ip->ttl > SAARSEC_MAX_TTL) { 21 | // Checksum can only be repaired given 2 bytes. Take old/new of ttl+protocol (2x 1byte) 22 | __u8 new_ttl = SAARSEC_MAX_TTL; 23 | __u16 old = *((__u16*) &ip->ttl); 24 | __u16 new = old; 25 | ((__u8*) &new)[0] = SAARSEC_MAX_TTL; 26 | bpf_l3_csum_replace(skb, ipoffset + offsetof(struct iphdr, check), old, new, 2); 27 | // Store only 1 byte (ttl) 28 | bpf_skb_store_bytes(skb, ipoffset + offsetof(struct iphdr, ttl), &new_ttl, 1, 0); 29 | } 30 | 31 | return TC_ACT_UNSPEC; 32 | } 33 | 34 | 35 | SEC("anonymize_traffic") 36 | int handle_egress(struct __sk_buff *skb) { 37 | __u32 offset = get_ip4_offset(skb); 38 | if (offset == NO_OFFSET) 39 | return TC_ACT_UNSPEC; 40 | else 41 | return anonymize_frame(skb, offset); 42 | } 43 | 44 | 45 | char __license[] SEC("license") = "GPL"; 46 | -------------------------------------------------------------------------------- /vpn/bpf/anonymize_traffic.o: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarkusBauer/saarctf-gameserver/88bee31ca3bf56492714eac5d0c6f96bc1a92079/vpn/bpf/anonymize_traffic.o -------------------------------------------------------------------------------- /vpn/bpf/install-gameserver.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Run manually on the internal interface(s) 4 | # Argument 1: Interface (default: autodetect) 5 | 6 | set -eu 7 | 8 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" 9 | 10 | # find interface 11 | if [ $# -eq 0 ]; then 12 | dev=$(ip -o -4 addr show | grep 10.32.250.1 | grep -oP '\d+:\s+\K\w+') 13 | if [ -z "$dev" ]; then 14 | echo "No interface given/found" 15 | exit 1 16 | fi 17 | else 18 | dev=$1 19 | fi 20 | 21 | tc qdisc del dev $dev clsact 2>/dev/null || true 22 | tc qdisc add dev $dev clsact 23 | tc filter del dev $dev ingress 24 | tc filter add dev $dev ingress bpf object-file "${DIR}/traffic_stats_gameserver.o" sec traffic_stats_gameserver_ingress direct-action 25 | echo "Added gameserver bpf to interface $dev" 26 | -------------------------------------------------------------------------------- /vpn/bpf/install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Argument 1: Team ID 4 | # Defined by OpenVPN: $dev 5 | 6 | set -e 7 | 8 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" 9 | 10 | tc qdisc del dev $dev clsact 2>/dev/null || true 11 | tc qdisc add dev $dev clsact 12 | 13 | tc filter del dev $dev egress 14 | tc filter del dev $dev ingress 15 | tc filter add dev $dev egress bpf object-file "${DIR}/anonymize_traffic.o" sec anonymize_traffic direct-action 16 | # tc filter add dev $dev ingress bpf object-file "${DIR}/anonymize_traffic.o" sec anonymize_traffic direct-action 17 | tc filter add dev $dev egress bpf object-file "${DIR}/traffic_stats.o" sec traffic_stats_egress direct-action 18 | tc filter add dev $dev ingress bpf object-file "${DIR}/traffic_stats.o" sec traffic_stats_ingress direct-action 19 | -------------------------------------------------------------------------------- /vpn/bpf/traffic_stats.o: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarkusBauer/saarctf-gameserver/88bee31ca3bf56492714eac5d0c6f96bc1a92079/vpn/bpf/traffic_stats.o -------------------------------------------------------------------------------- /vpn/bpf/traffic_stats_gameserver.o: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarkusBauer/saarctf-gameserver/88bee31ca3bf56492714eac5d0c6f96bc1a92079/vpn/bpf/traffic_stats_gameserver.o -------------------------------------------------------------------------------- /vpn/bpf/uninstall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | # tc qdisc del dev eth0 clsact 6 | tc filter del dev $dev egress 7 | tc filter del dev $dev ingress 8 | tc filter show dev $dev egress 9 | tc filter show dev $dev ingress 10 | -------------------------------------------------------------------------------- /vpn/on-cloud-connect.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os 3 | import sys 4 | import traceback 5 | 6 | sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 7 | 8 | from controlserver.models import Team, db_session, init_database 9 | from saarctf_commons.config import config, load_default_config 10 | 11 | """ 12 | Increments the "cloud" connection counter for a team. Called as "up" script from OpenVPN. 13 | ARGUMENTS: Team-ID 14 | """ 15 | 16 | 17 | def main(): 18 | team_id = int(sys.argv[1]) 19 | changes = Team.query.filter(Team.id == team_id).update(dict(vpn_connection_count=Team.vpn_connection_count + 1), synchronize_session=False) 20 | db_session().commit() 21 | if changes > 0: 22 | print(f'Updated connection status (connected) of team #{team_id}.') 23 | elif team_id > 0: 24 | session = db_session() 25 | session.add(Team(id=team_id, name=f'unnamed team #{team_id}', vpn_connection_count=1)) 26 | session.commit() 27 | print(f'Updated connection status (connected) of team #{team_id}. Created new dummy team entry for that.') 28 | else: 29 | print(f'Team #{team_id} not found.') 30 | 31 | 32 | if __name__ == '__main__': 33 | try: 34 | load_default_config() 35 | config.set_script() 36 | init_database() 37 | main() 38 | except Exception as e: 39 | with open('/tmp/connect-error.log', 'a') as f: 40 | f.write('=== CONNECT ===') 41 | f.write(repr(sys.argv)) 42 | traceback.print_exc(file=f) 43 | f.write('\n') 44 | raise 45 | -------------------------------------------------------------------------------- /vpn/on-cloud-disconnect.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os 3 | import sys 4 | import traceback 5 | 6 | from sqlalchemy import func 7 | 8 | sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 9 | 10 | from controlserver.models import Team, db_session, init_database 11 | from saarctf_commons.config import config, load_default_config 12 | 13 | """ 14 | Decrements the "cloud" connection counter for a team. Called as "up" script from OpenVPN. 15 | ARGUMENTS: Team-ID 16 | """ 17 | 18 | 19 | def main(): 20 | team_id = int(sys.argv[1]) 21 | changes = Team.query.filter(Team.id == team_id).update(dict(vpn_connection_count=Team.vpn_connection_count - 1), synchronize_session=False) 22 | db_session().commit() 23 | if changes > 0: 24 | print(f'Updated connection status (disconnected) of team #{team_id}.') 25 | else: 26 | print(f'Team #{team_id} not found.') 27 | 28 | 29 | if __name__ == '__main__': 30 | try: 31 | load_default_config() 32 | config.set_script() 33 | init_database() 34 | main() 35 | except Exception as e: 36 | with open('/tmp/connect-error.log', 'a') as f: 37 | f.write('=== DISCONNECT ===') 38 | f.write(repr(sys.argv)) 39 | traceback.print_exc(file=f) 40 | f.write('\n') 41 | raise 42 | -------------------------------------------------------------------------------- /vpn/on-cloud-down.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os 3 | import sys 4 | import traceback 5 | 6 | from sqlalchemy import func 7 | 8 | sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 9 | 10 | from controlserver.models import Team, db_session, init_database 11 | from saarctf_commons.config import config, load_default_config 12 | 13 | """ 14 | Decrements the "cloud" connection counter for a team. Called as "down" script from OpenVPN. 15 | ARGUMENTS: Team-ID 16 | """ 17 | 18 | 19 | def main(): 20 | team_id = int(sys.argv[1]) 21 | changes = Team.query.filter(Team.id == team_id).update(dict(vpn_connection_count=0), synchronize_session=False) 22 | db_session().commit() 23 | if changes > 0: 24 | print(f'Updated connection status (down) of team #{team_id}.') 25 | else: 26 | print(f'Team #{team_id} not found.') 27 | 28 | 29 | if __name__ == '__main__': 30 | try: 31 | load_default_config() 32 | config.set_script() 33 | init_database() 34 | main() 35 | except Exception as e: 36 | with open('/tmp/connect-error.log', 'a') as f: 37 | f.write('=== DOWN ===') 38 | f.write(repr(sys.argv)) 39 | traceback.print_exc(file=f) 40 | f.write('\n') 41 | raise 42 | -------------------------------------------------------------------------------- /vpn/on-connect.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Called as "up" script from OpenVPN - set device options and mark team as "online" for the gameserver 4 | # $1 = team ID 5 | # $2 = "teamhosted" or not present 6 | 7 | set -e 8 | 9 | cd "$( dirname "${BASH_SOURCE[0]}" )" 10 | # Load bpf program on interface 11 | bpf/install.sh "$1" 12 | # Install rate-limiting queue on interface 13 | ratelimit/install.sh "$1" 14 | # Mark in database as connected 15 | gspython $(pwd)/on-connect.py "$1" "$2" 16 | 17 | if [ "$2" = "teamhosted" ]; then 18 | systemctl stop "vpn2@team$1-cloud" 19 | fi 20 | 21 | # Possibly prevent some bugs. When people start a cloud VM, we get a connection to vulnbox-VPN, which should enable cloud-vpn and disable team-hosted vpn 22 | if [ "$2" = "cloudhosted" ]; then 23 | systemctl stop "vpn@team$1" 24 | systemctl start "vpn2@team$1-cloud" || true 25 | fi 26 | -------------------------------------------------------------------------------- /vpn/on-device-up.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Called as "up" script from OpenVPN - set device options and mark team as "online" for the gameserver 4 | 5 | set -e 6 | 7 | cd "$( dirname "${BASH_SOURCE[0]}" )" 8 | # Load bpf program on interface 9 | bpf/install.sh "$1" 10 | # Install rate-limiting queue on interface 11 | ratelimit/install.sh "$1" 12 | -------------------------------------------------------------------------------- /vpn/on-disconnect.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Called as "down" script from OpenVPN - set device options and mark team as "offline" for the gameserver 4 | # $1 = team ID or "teamXY" or "teamXY-vulnbox" 5 | # $2 = "teamhosted" or not present 6 | 7 | set -e 8 | 9 | cd "$( dirname "${BASH_SOURCE[0]}" )" 10 | 11 | # Mark in database as disconnected 12 | gspython $(pwd)/on-disconnect.py "$1" "$2" 13 | 14 | TEAMID=$(echo "$1" | sed 's/[^0-9]*//g') 15 | if [ "$2" = "teamhosted" ]; then 16 | systemctl start "vpn2@team$TEAMID-cloud" || true 17 | fi 18 | if [ "$2" = "cloudhosted" ]; then 19 | systemctl start "vpn@team$TEAMID" || true 20 | fi 21 | -------------------------------------------------------------------------------- /vpn/ratelimit/reapply-all.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | INTERFACES=$(ip -o link show | awk -F': ' '{print $2}' | grep tun) 6 | 7 | for i in $INTERFACES; do 8 | echo "- Apply rates on $i ..." 9 | dev=$i ./install.sh 10 | done 11 | 12 | echo "[DONE!]" 13 | -------------------------------------------------------------------------------- /vpn/tcpdump/move-gametraffic.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | # TODO some config here? Or just use a symlink? 6 | FOLDER="/tmp/gametraffic" 7 | 8 | mkdir -p "$FOLDER" 9 | mv "$1" "$FOLDER/" 10 | -------------------------------------------------------------------------------- /vpn/tcpdump/move-teamtraffic.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | # TODO some config here? Or just use a symlink? 6 | FOLDER="/tmp/teamtraffic" 7 | 8 | mkdir -p "$FOLDER" 9 | mv "$1" "$FOLDER/" 10 | -------------------------------------------------------------------------------- /vpn/tcpdump/run-tcpdump.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -eu 3 | 4 | # ARGUMENT 1: "game" or "team" 5 | # to dump team<->game or team<->team traffic 6 | # ARGUMENT 2: service ID (for "team" only) 7 | 8 | 9 | if [[ "$1" == "game" ]]; then 10 | interface="nflog:5" 11 | filename_scheme="traffic_game_%Y-%m-%d_%H_%M_%S.pcap" 12 | elif [[ "$1" == "team" ]]; then 13 | interface="nflog:$((10+$2))" 14 | filename_scheme="traffic_team_%Y-%m-%d_%H_%M_%S_svc$(printf "%02d" $2).pcap" 15 | else 16 | echo 'Error: Argument must be either "game" or "team".' >&2 17 | exit 1 18 | fi 19 | 20 | 21 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" 22 | 23 | # TODO configure? 24 | if [ -d "/tmp/temptraffic/" ]; then 25 | FOLDER="/tmp/temptraffic" 26 | else 27 | FOLDER="/tmp" 28 | fi 29 | 30 | exec tcpdump -i "$interface" -s0 \ 31 | -B 131072 \ 32 | -G 60 -w "$FOLDER/$filename_scheme" \ 33 | -z "$DIR/move-${1}traffic.sh" -Z nobody 34 | -------------------------------------------------------------------------------- /vpn/tcpdump/setup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Run this script once before starting tcpdump, to create all folders 4 | 5 | set -e 6 | 7 | # TODO configurable? 8 | mkdir -p /tmp 9 | mkdir -p /tmp/teamtraffic 10 | mkdir -p /tmp/gametraffic 11 | chown nobody:nogroup /tmp/teamtraffic 12 | chown nobody:nogroup /tmp/gametraffic 13 | -------------------------------------------------------------------------------- /vpn/test/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:bullseye 2 | 3 | RUN apt-get update && \ 4 | apt-get install -y \ 5 | openvpn htop nano sudo screen \ 6 | net-tools iptables bash-completion iputils-ping tcpdump netcat-openbsd \ 7 | python3 socat && \ 8 | apt-get clean && \ 9 | echo 'shell "/bin/bash"' > ~/.screenrc 10 | 11 | CMD ["socat", "-T", "10", "tcp-l:12345,reuseaddr,fork", "exec:'/bin/cat'"] 12 | -------------------------------------------------------------------------------- /vpn/test/docker-compose.yml: -------------------------------------------------------------------------------- 1 | # Patch all configs: sed -i 's|remote vpn.ctf.saarland|remote 192.168.178.93|' config-client/*.conf 2 | # SHELL IN CONTAINER: docker exec -it test_vpn-team1-self_1 bash 3 | # CONNECT VPN: openvpn /vpn.conf 4 | # Bind router's IP: ifconfig lo:1 10.32.1.1 netmask 255.255.255.255 up 5 | 6 | version: "2.4" 7 | services: 8 | vpn-generic: 9 | build: . 10 | cap_add: 11 | - NET_ADMIN 12 | devices: 13 | - /dev/net/tun 14 | volumes: 15 | - "/:/mnt" 16 | 17 | vpn-team1-self: 18 | extends: 19 | service: vpn-generic 20 | volumes: 21 | - "../config-client/client-team1.conf:/vpn.conf" 22 | vpn-team1-vuln: 23 | extends: 24 | service: vpn-generic 25 | volumes: 26 | - "../config-client/client-team1-vulnbox.conf:/vpn.conf" 27 | vpn-team1-cloud: 28 | extends: 29 | service: vpn-generic 30 | volumes: 31 | - "../config-client/client-cloud-team1.conf:/vpn.conf" 32 | vpn-team1-cloud2: 33 | extends: 34 | service: vpn-generic 35 | volumes: 36 | - "../config-client/client-cloud-team1.conf:/vpn.conf" 37 | 38 | vpn-team2-self: 39 | extends: 40 | service: vpn-generic 41 | volumes: 42 | - "../config-client/client-team2.conf:/vpn.conf" 43 | vpn-team2-vuln: 44 | extends: 45 | service: vpn-generic 46 | volumes: 47 | - "../config-client/client-team2-vulnbox.conf:/vpn.conf" 48 | vpn-team2-cloud: 49 | extends: 50 | service: vpn-generic 51 | volumes: 52 | - "../config-client/client-cloud-team2.conf:/vpn.conf" -------------------------------------------------------------------------------- /vpn/test/test-client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import socket 4 | import sys 5 | 6 | PORT = int(sys.argv[2]) if len(sys.argv) > 2 else 1234 7 | MESSAGE = sys.argv[3] if len(sys.argv) > 3 else '' 8 | 9 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 10 | s.settimeout(7) 11 | s.connect((sys.argv[1], PORT)) 12 | s.sendall(MESSAGE.encode('utf-8')) 13 | data = s.recv(4096) 14 | s.close() 15 | print(data.decode()) 16 | -------------------------------------------------------------------------------- /vpn/test/test-server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import socket 4 | import sys 5 | 6 | PORT = int(sys.argv[1]) if len(sys.argv) > 1 else 1234 7 | 8 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 9 | s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) 10 | s.bind(('0.0.0.0', PORT)) 11 | s.listen(5) 12 | print(f'Listening on port {PORT} ...') 13 | 14 | while True: 15 | conn, addr = s.accept() 16 | conn.settimeout(7) 17 | data = conn.recv(4096) 18 | print(f'Connection from {addr}: "{data.decode("utf-8")}"') 19 | response = f'[OK] with {addr[0]}:{addr[1]}\nMessage: "{data.decode()}"' 20 | if len(sys.argv) > 2: 21 | response += ' from '+sys.argv[2] 22 | conn.send(response.encode()) 23 | conn.close() 24 | -------------------------------------------------------------------------------- /vpn/useful-commands.txt: -------------------------------------------------------------------------------- 1 | 2 | # "Just bind" an IP 3 | ifconfig lo:1 10.32.1.1 netmask 255.255.255.255 up 4 | ifconfig lo:1 10.32.2.1 netmask 255.255.255.255 up 5 | 6 | # Debug IPTables 7 | iptables -nvL 8 | watch -n1 iptables -nvL 9 | iptables -Z # clear 10 | -------------------------------------------------------------------------------- /vpnboard/__init__.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from dataclasses import dataclass 3 | 4 | from controlserver.models import Team 5 | from vpnboard.wg_status import WgStatus 6 | 7 | 8 | @dataclass 9 | class VpnStatus: 10 | team: Team 11 | wg: WgStatus | None = None 12 | router_ping_ms: float | None = None 13 | testbox_ping_ms: float | None = None 14 | testbox_ok: bool = False 15 | testbox_err: str | None = None 16 | vulnbox_ping_ms: float | None = None # only filled if explicitly selected 17 | 18 | @property 19 | def connected(self) -> bool: 20 | return self.team.vpn_connected or self.team.vpn2_connected or self.team.wg_boxes_connected 21 | 22 | 23 | class VpnStatusHandler(ABC): 24 | @abstractmethod 25 | def update(self, states: list[VpnStatus], banned_teams: set[int], check_vulnboxes: bool, start: float) -> None: 26 | raise NotImplementedError() 27 | -------------------------------------------------------------------------------- /wireguard-sync/.env.example: -------------------------------------------------------------------------------- 1 | # Required 2 | # Set as `ROUTER_TOKEN` in constance of saarctf webpage. 3 | API_TOKEN= 4 | 5 | # For available settings check wireguard_sync/settings.py 6 | -------------------------------------------------------------------------------- /wireguard-sync/README.md: -------------------------------------------------------------------------------- 1 | # About 2 | 3 | Asyncio python tool that synchronizes local wireguard interfaces with configuration 4 | provided by players in saarctf-webapge. 5 | 6 | # WARNING 7 | 8 | This thing fucks around with your network settings, don't run it on your local 9 | machine if you don't know what you are doing. Depending on load it also might do a lot 10 | of `fsync`. 11 | 12 | # Setup 13 | 14 | Poetry dependencies and Python3.12. 15 | `poetry install` 16 | 17 | # Usage 18 | 19 | Configure via env vars or `.env` file in working dir. 20 | For available / required vars, check `.env.example`. 21 | 22 | `python wireguard_sync` 23 | 24 | # Development 25 | 26 | Docker compose setup connects to a local docker-compose instance of saarctf-webpage 27 | and creates the interfaces in a docker container. 28 | -------------------------------------------------------------------------------- /wireguard-sync/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.12-alpine3.20 AS poetry 2 | 3 | RUN apk add py3-virtualenv 4 | WORKDIR /opt/poetry 5 | RUN python3 -m venv venv 6 | ENV VIRTUAL_ENV=/opt/poetry/venv 7 | ENV PATH=$VIRTUAL_ENV/bin:$PATH 8 | RUN pip install poetry 9 | 10 | FROM python:3.12-alpine3.20 11 | 12 | RUN apk add wireguard-tools iproute2 13 | 14 | COPY --from=poetry /opt/poetry/venv /opt/poetry/venv 15 | RUN ln -s /opt/poetry/venv/bin/poetry /bin/poetry 16 | 17 | WORKDIR /opt/saarctf/ 18 | RUN python3 -m venv ./venv 19 | ENV VIRTUAL_ENV=/opt/saarctf/venv 20 | ENV PATH=$VIRTUAL_ENV/bin:$PATH 21 | 22 | WORKDIR /opt/saarctf/router 23 | COPY pyproject.toml . 24 | COPY poetry.lock . 25 | 26 | RUN poetry install --with dev --no-root --compile 27 | COPY wireguard_sync wireguard_sync 28 | RUN poetry install 29 | -------------------------------------------------------------------------------- /wireguard-sync/docker/README.md: -------------------------------------------------------------------------------- 1 | # About 2 | 3 | Compose setup to let this code actually manage interfaces without having run hundredts 4 | of interfaces on your host. Only for development purposes! 5 | You need to start the webpage in its docker compose setup so the sync script has 6 | something to talk to. 7 | -------------------------------------------------------------------------------- /wireguard-sync/docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | networks: 2 | default: 3 | name: saarctf-local_default 4 | external: true 5 | 6 | services: 7 | # This container owns the network namespace and can stay up when restarting 8 | # wg-sync, this way wg-interfaces aren't deleted every time you restart the 9 | # container 10 | network-dummy: 11 | image: alpine:3.20 12 | command: tail -f /dev/null 13 | stop_grace_period: 1s 14 | 15 | wg-sync: 16 | image: saarctf/vpn 17 | build: 18 | context: .. 19 | dockerfile: docker/Dockerfile 20 | network_mode: service:network-dummy 21 | cap_add: 22 | - NET_ADMIN 23 | - NET_RAW 24 | volumes: 25 | - ../wireguard_sync:/opt/saarctf/router/wireguard_sync 26 | environment: 27 | API_TOKEN: ${API_TOKEN} 28 | API_SERVER: "http://caddy:8000" 29 | stop_grace_period: 1s 30 | command: python wireguard_sync -------------------------------------------------------------------------------- /wireguard-sync/pyproject.toml: -------------------------------------------------------------------------------- 1 | # This file is used to host wireguard-sync in an independent setting 2 | # During saarctf, we'll install dependencies using the main project's "make deps" mechanism 3 | 4 | [tool.poetry] 5 | name = "wireguard-sync" 6 | version = "0.1.0" 7 | description = "" 8 | authors = ["SaarCTF maintainers "] 9 | 10 | [tool.poetry.dependencies] 11 | python = "^3.12" 12 | aiohttp = "^3.11.7" 13 | python-dotenv = "^1.0.1" 14 | pyroute2 = "^0.7.12" 15 | cryptography = "^43.0.3" 16 | 17 | [tool.poetry.group.dev.dependencies] 18 | ruff = "^0.8.0" 19 | mypy = "^1.13.0" 20 | pytest = "^8.3.3" 21 | 22 | [build-system] 23 | requires = ["poetry-core"] 24 | build-backend = "poetry.core.masonry.api" 25 | 26 | [tool.ruff] 27 | line-length = 128 28 | 29 | [[tool.mypy.overrides]] 30 | module = ["pyroute2.*", "saarctf_commons"] 31 | ignore_missing_imports = true 32 | -------------------------------------------------------------------------------- /wireguard-sync/wireguard_sync/exceptions.py: -------------------------------------------------------------------------------- 1 | class WGSyncException(Exception): ... 2 | 3 | 4 | class ConfigurationError(WGSyncException): ... 5 | 6 | 7 | class InterfaceDoesNotExist(WGSyncException): ... 8 | 9 | 10 | class ApiError(WGSyncException): ... 11 | -------------------------------------------------------------------------------- /wireguard-sync/wireguard_sync/rest_api.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contains the type definitions for the REST API. 3 | """ 4 | 5 | from typing import TypedDict 6 | 7 | 8 | class KeySlot(TypedDict): 9 | public_key: str 10 | 11 | 12 | class Peer(TypedDict): 13 | key_slot: KeySlot 14 | cidr: str 15 | 16 | 17 | class MinimalInterface(TypedDict): 18 | id: int 19 | 20 | 21 | class Interface(MinimalInterface): 22 | cidr: str 23 | public_key: str | None 24 | last_modified: str | None 25 | peers: list[Peer] 26 | port: int 27 | -------------------------------------------------------------------------------- /wireguard-sync/wireguard_sync/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pathlib import Path 3 | 4 | from dotenv import load_dotenv 5 | 6 | load_dotenv() 7 | 8 | API_SERVER = os.getenv("API_SERVER", "http://ctf.localhost") 9 | API_BASE = os.getenv("API_BASE", "/api/router/") 10 | API_TOKEN = os.getenv("API_TOKEN", None) 11 | API_CONCURRENCY = int(os.getenv("API_CONCURRENCY", 1)) 12 | 13 | KEYSTORE_PATH: str = os.getenv("KEYSTORE_PATH", "./keystore.json") 14 | 15 | BASE_DIR: Path = Path(__file__).parent 16 | INTERFACE_UP_HOOKS: list[Path] = [ 17 | BASE_DIR / "../../vpn/bpf/install.sh", 18 | BASE_DIR / "../../vpn/ratelimit/install.sh", 19 | ] 20 | 21 | # import config from saarctf common config, if available 22 | try: 23 | from saarctf_commons import config 24 | 25 | config.load_default_config() 26 | wg = config.current_config.WIREGUARD_SYNC 27 | if wg: 28 | API_SERVER = wg.api_server 29 | API_BASE = wg.api_base 30 | API_TOKEN = wg.api_token 31 | API_CONCURRENCY = wg.api_concurrency 32 | KEYSTORE_PATH = str(config.current_config.basedir / "keystore.json") 33 | print("Imported saarctf settings") 34 | else: 35 | print("Wireguard sync not configured in saarctf settings") 36 | except ImportError: 37 | print("No saarctf settings available") 38 | -------------------------------------------------------------------------------- /wireguard-sync/wireguard_sync/test-api.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 10, 3 | "cidr": "10.32.11.0/24", 4 | "last_modified": "2024-11-22T14:43:02.282846Z", 5 | "last_synced": null, 6 | "port": 32010, 7 | "peers": [ 8 | { 9 | "key_slot": { 10 | "public_key": "GlrIoQh1cHo9+PYt+lPqap8VcGfnhbvt6dmGlxpxU28=" 11 | }, 12 | "cidr": "10.10.11.100/32" 13 | }, 14 | { 15 | "key_slot": { 16 | "public_key": "LEGSGepT9di1yScfH21AF2HneLw8/0yTtTnowYWMy38=" 17 | }, 18 | "cidr": "10.10.11.101/32" 19 | }, 20 | { 21 | "key_slot": { 22 | "public_key": "ShSXVB1Ih5uoByubvBb/AGhHfzdp8hhjIsk817FpgEQ=" 23 | }, 24 | "cidr": "10.10.11.102/32" 25 | }, 26 | { 27 | "key_slot": { 28 | "public_key": "zy0JCMB51vPlJp+unGP1x1MZZf42/1TxlgRdwqwJSlQ=" 29 | }, 30 | "cidr": "10.10.11.103/32" 31 | }, 32 | { 33 | "key_slot": { 34 | "public_key": "SEcElwBKrmiVRM0QoWOiy6JI4C3/oarXC1HhXmV18j4=" 35 | }, 36 | "cidr": "10.10.11.104/32" 37 | }, 38 | { 39 | "key_slot": { 40 | "public_key": "Au5y1Rf+QTmnExz1A8/HkDzHf/XloiSP0CQLeD1OG3E=" 41 | }, 42 | "cidr": "10.10.11.105/32" 43 | }, 44 | { 45 | "key_slot": { 46 | "public_key": "qnyB4GP3LvkQ48KrC5UgQXu6ShViWh7mC5fvASxGzlE=" 47 | }, 48 | "cidr": "10.10.11.106/32" 49 | } 50 | ], 51 | "public_key": null 52 | } 53 | -------------------------------------------------------------------------------- /wireguard-sync/wireguard_sync/utils.py: -------------------------------------------------------------------------------- 1 | from logging import config, root 2 | 3 | 4 | def force_schema(url_or_hostname: str) -> str: 5 | """ 6 | Force a "url" to have a schema, if none is present, guess https://. 7 | """ 8 | if url_or_hostname.startswith("http://") or url_or_hostname.startswith("https://"): 9 | return url_or_hostname 10 | else: 11 | return f"https://{url_or_hostname}" 12 | 13 | 14 | LOGGING = { 15 | "version": 1, 16 | "disable_existing_loggers": False, 17 | "handlers": { 18 | "console": { 19 | "formatter": "default_formatter", 20 | "class": "logging.StreamHandler", 21 | }, 22 | }, 23 | "root": { 24 | "handlers": ["console"], 25 | "level": "INFO", 26 | }, 27 | "loggers": {}, 28 | "formatters": { 29 | "default_formatter": { 30 | "format": "%(asctime)s | %(levelname)s | %(message)s | %(name)s | " "%(filename)s:%(lineno)s", 31 | }, 32 | }, 33 | } 34 | 35 | 36 | def configure_logging() -> None: 37 | config.dictConfig(LOGGING) 38 | 39 | try: 40 | from saarctf_commons.logging_utils import add_ecs_logging, DefaultAttributesFilter 41 | root.addFilter(DefaultAttributesFilter({"event.source": 'wireguard-sync'})) 42 | add_ecs_logging() 43 | except ImportError: 44 | pass 45 | --------------------------------------------------------------------------------