diff --git a/.env b/.env new file mode 100644 index 00000000..fba7692a --- /dev/null +++ b/.env @@ -0,0 +1 @@ +COMPOSE_PROJECT_NAME=hlstatsx-community-edition diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml new file mode 100644 index 00000000..80c8b7b2 --- /dev/null +++ b/.github/release-drafter.yml @@ -0,0 +1,49 @@ +name-template: 'v$RESOLVED_VERSION 🌈' +tag-template: 'v$RESOLVED_VERSION' +categories: + - title: '🚀 Features' + labels: + - 'feature' + - 'enhancement' + - 'change' + - title: '🐛 Bug Fixes' + labels: + - 'fix' + - 'bug' + - title: '🖊️ Refactors' + labels: + - 'refactor' + - title: '👗 Style' + labels: + - 'style' + - title: '📝 Documentation' + labels: + - 'docs' + - 'documentation' + - title: '🧰 Maintenance' + label: 'chore' +change-template: '- $TITLE @$AUTHOR (#$NUMBER)' +version-resolver: + major: + labels: + - 'breaking' + minor: + labels: + - 'feature' + - 'enhancement' + - 'change' + - 'refactor' + patch: + labels: + - 'fix' + - 'bug' + - 'style' + - 'docs' + - 'documentation' + - 'chore' + default: patch +sort-by: title +template: | + ## Changes + + $CHANGES diff --git a/.github/workflows/ci-master-pr.yml b/.github/workflows/ci-master-pr.yml new file mode 100644 index 00000000..2e9ce2cd --- /dev/null +++ b/.github/workflows/ci-master-pr.yml @@ -0,0 +1,189 @@ +name: ci-master-pr + +on: + push: + branches: + - master + tags: + - '**' + pull_request: + branches: + - master + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Set up Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v2 + + - name: Cache Docker layers + uses: actions/cache@v3 + with: + path: /tmp/.buildx-cache + key: ${{ runner.os }}-buildx-test-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-buildx-test- + + - name: Print buildx and compose + run: | + set -eu + docker buildx ls + docker compose version + + - name: Test (integration) + run: | + set -eu + docker compose up --build -d + docker compose -f docker-compose.test.yml up + + build: + strategy: + matrix: + variant: + - web-nginx + - web-php + - daemon + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Display system info (linux) + run: | + set -e + hostname + whoami + cat /etc/*release + lscpu + free + df -h + pwd + docker info + docker version + + # See: https://github.com/docker/build-push-action/blob/v2.6.1/docs/advanced/cache.md#github-cache + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + + - name: Set up Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v2 + + - name: Cache Docker layers + uses: actions/cache@v3 + with: + path: /tmp/.buildx-cache + key: ${{ runner.os }}-buildx-${{ matrix.variant }}-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-buildx-${{ matrix.variant }}- + ${{ runner.os }}-buildx- + + # This step generates the docker tags + - name: Docker meta + id: meta + uses: docker/metadata-action@v4 + with: + images: | + ${{ github.repository }} + # type=ref,event=pr generates tag(s) on PRs only. E.g. 'pr-123-', 'pr-123-abc0123-' + # type=ref,event=branch generates tag(s) on branch only. E.g. 'master-', 'master-abc0123-' + # type=ref,event=tag generates tag(s) on tags only. E.g. 'v0.0.0-', 'v0.0.0-abc0123-' + tags: | + type=ref,suffix=-${{ matrix.variant }},event=pr + type=ref,suffix=-{{sha}}-${{ matrix.variant }},event=pr + type=ref,suffix=-${{ matrix.variant }},event=branch + type=ref,suffix=-{{sha}}-${{ matrix.variant }},event=branch + type=ref,suffix=-${{ matrix.variant }},event=tag + type=ref,suffix=-{{sha}}-${{ matrix.variant }},event=tag + # Disable 'latest' tag + flavor: | + latest=false + + - name: Login to Docker Hub registry + # Run on master and tags + if: github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/') + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_REGISTRY_USER }} + password: ${{ secrets.DOCKERHUB_REGISTRY_PASSWORD }} + + - name: Build (PRs) + # Run on pull requests + if: github.event_name == 'pull_request' + uses: docker/build-push-action@v3 + with: + file: Dockerfile.${{ matrix.variant }} + context: '.' + platforms: linux/amd64 + push: false + tags: ${{ steps.meta.outputs.tags }} + cache-from: type=local,src=/tmp/.buildx-cache + cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max + + - name: Build and push + # Run on master and tags + if: github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/') + uses: docker/build-push-action@v3 + with: + file: Dockerfile.${{ matrix.variant }} + context: '.' + platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/s390x + push: true + tags: ${{ steps.meta.outputs.tags }} + cache-from: type=local,src=/tmp/.buildx-cache + cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max + + # Temp fix + # https://github.com/docker/build-push-action/issues/252 + # https://github.com/moby/buildkit/issues/1896 + - name: Move cache + run: | + rm -rf /tmp/.buildx-cache + mv /tmp/.buildx-cache-new /tmp/.buildx-cache + + update-draft-release: + needs: [test, build] + if: github.ref == 'refs/heads/master' + runs-on: ubuntu-latest + steps: + # Drafts your next Release notes as Pull Requests are merged into "master" + - uses: release-drafter/release-drafter@v5 + with: + config-name: release-drafter.yml + publish: false + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + publish-draft-release: + needs: [test, build] + if: startsWith(github.ref, 'refs/tags/') + runs-on: ubuntu-latest + steps: + # Drafts your next Release notes as Pull Requests are merged into "master" + - uses: release-drafter/release-drafter@v5 + with: + config-name: release-drafter.yml + publish: true + name: ${{ github.ref_name }} # E.g. 'master' or 'v1.2.3' + tag: ${{ github.ref_name }} # E.g. 'master' or 'v1.2.3' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + update-dockerhub-description: + needs: [test, build] + if: github.ref == 'refs/heads/master' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Docker Hub Description + uses: peter-evans/dockerhub-description@v3 + with: + username: ${{ secrets.DOCKERHUB_REGISTRY_USER }} + password: ${{ secrets.DOCKERHUB_REGISTRY_PASSWORD }} + repository: ${{ github.repository }} + short-description: ${{ github.event.repository.description }} diff --git a/.gitignore b/.gitignore index 26638d72..355e196e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,7 @@ /.idea /web/.idea -/.vscode -/.vagrant \ No newline at end of file +/.vscode/* +!/.vscode/launch.json +/.vagrant +/web/hlstatsimg/progress +/web/hlstatsimg/graph diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..5d915721 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,29 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Listen for XDebug", + "type": "php", + "request": "launch", + "port": 9000, + "pathMappings": { + "/web": "${workspaceRoot}/web", + }, + "xdebugSettings": { + "max_data": 10000, + "max_children": 10000 + } + }, + { + "name": "Launch currently open script", + "type": "php", + "request": "launch", + "program": "${file}", + "cwd": "${fileDirname}", + "port": 9000 + } + ] +} diff --git a/Dockerfile.daemon b/Dockerfile.daemon new file mode 100644 index 00000000..c28f138c --- /dev/null +++ b/Dockerfile.daemon @@ -0,0 +1,86 @@ +FROM perl:5.38.0-slim-buster AS base + +# Install modules +RUN set -eux; \ + apt-get update; \ + apt-get install -y build-essential; \ + \ + cpanm DBI; \ + \ + # Install DBD::mysql, requires mysql 8. See: https://stackoverflow.com/q/4729722 + apt-get update; \ + apt-get install -y wget; \ + # See: https://dev.mysql.com/downloads/repo/apt/ + wget -q https://dev.mysql.com/get/mysql-apt-config_0.8.28-1_all.deb; \ + DEBIAN_FRONTEND=noninteractive apt install -y ./mysql-apt-config_*; \ + apt-get update; \ + apt-get install -y libmysqlclient-dev; \ + apt-get install -y libmysqlclient21; \ + cpanm DBD::mysql; \ + # apt purge --auto-remove -y mysql-community-client; \ + # apt purge --auto-remove -y mysql-server; \ + apt purge --auto-remove -y libmysqlclient-dev; \ + apt purge --auto-remove -y mysql-apt-config; \ + rm -fv ./mysql-apt-config_*; \ + \ + cpanm Geo::IP::PurePerl; \ + cpanm GeoIP2::Database::Reader; \ + cpanm MaxMind::DB::Reader; \ + cpanm Syntax::Keyword::Try; \ + \ + # Optional modules: Email + cpanm Email::Sender::Simple; \ + apt-get purge --auto-remove -y build-essential; \ + rm -rf /var/lib/apt/lists/* /root/.cpan/ /root/.cpanm/ + +# Install tools +RUN set -eux; \ + apt-get update; \ + apt-get install -y \ + procps \ + # Cron tools + cron \ + curl \ + wget \ + openssl \ + && rm -rf /var/lib/apt/lists/* + +RUN set -eux; \ + mkdir -p /scripts /scripts/GeoLiteCity; \ + cd /scripts/GeoLiteCity; \ + # Download the GeoIP binary. Maxmind discontinued distributing the GeoLite Legacy databases. See: https://support.maxmind.com/geolite-legacy-discontinuation-notice/ + # So let's download it from our fork of GeoLiteCity.dat + wget -qO- https://github.com/startersclan/GeoLiteCity-data/raw/c14d99c42446f586e3ca9c89fe13714474921d65/GeoLiteCity.dat > GeoLiteCity.dat; \ + chmod 666 GeoLiteCity.dat; \ + # Download the GeoIP2 binary. Maxmind discontinued distributing the GeoLite2 databases publicly, so a license key is needed. See: https://blog.maxmind.com/2019/12/18/significant-changes-to-accessing-and-using-geolite2-databases/ + # In order to obtain the secret MAXMIND_LICENSE_KEY, we assume we have a sidecar secrets-server which will serve the secret MAXMIND_LICENSE_KEY at: http://localhost:8000/MAXMIND_LICENSE_KEY + wget -qO- https://cdn.jsdelivr.net/npm/geolite2-city@1.0.0/GeoLite2-City.mmdb.gz > GeoLite2-City.mmdb.gz; \ + gzip -d GeoLite2-City.mmdb.gz; \ + chmod 666 GeoLite2-City.mmdb; \ + ls -al + +# Copy scripts and set permissions +COPY scripts /scripts2 +RUN set -eux; \ + ls /scripts2 | grep -v GeoLiteCity | while read -r i; do mv -v "/scripts2/$i" /scripts; done; \ + mv -v /scripts2/GeoLiteCity/* /scripts/GeoLiteCity/; \ + rm -rf /scripts2; \ + find /scripts; \ + find /scripts -type d -exec chmod 750 {} \;; \ + find /scripts -type f -exec chmod 640 {} \;; \ + find /scripts -type f -name '*.sh' -exec chmod 750 {} \;; \ + find /scripts -type f -name '*.pl' -exec chmod 750 {} \;; \ + find /scripts -type f -name 'run_*' -exec chmod 750 {} \;; + +EXPOSE 27500/udp + +STOPSIGNAL SIGINT + +WORKDIR /scripts + +CMD ["perl", "./hlstats.pl"] + +RUN +FROM base AS dev + +FROM base AS prod diff --git a/Dockerfile.web-nginx b/Dockerfile.web-nginx new file mode 100644 index 00000000..8b38fd71 --- /dev/null +++ b/Dockerfile.web-nginx @@ -0,0 +1,18 @@ +FROM nginx:1.21-alpine AS base + +WORKDIR /web + +FROM base AS dev + +FROM base AS prod + +# Set permissions for 'nginx' user +# COPY --chown=nginx:nginx --chmod=640 /web /web +COPY ./web /web +RUN set -eux; \ + chown -R nginx:nginx /web; \ + find /web -type d -exec chmod 750 {} \; ; \ + find /web -type f -exec chmod 640 {} \; ; + +# Add default configs +COPY config/web/nginx/nginx.conf /etc/nginx/nginx.conf diff --git a/Dockerfile.web-php b/Dockerfile.web-php new file mode 100644 index 00000000..6ecf98ca --- /dev/null +++ b/Dockerfile.web-php @@ -0,0 +1,77 @@ +FROM php:8.1-fpm-alpine AS base +ARG TARGETPLATFORM +ARG BUILDPLATFORM +RUN echo "I am running on $BUILDPLATFORM, building for $TARGETPLATFORM" + +# opcache +RUN docker-php-ext-install opcache + +# mysqli (deprecated) +RUN set -eux; \ + docker-php-ext-install mysqli + +# gd +RUN set -eux; \ + apk add --no-cache freetype libjpeg-turbo libpng; \ + apk add --no-cache --virtual .deps freetype-dev libjpeg-turbo-dev libpng-dev; \ + docker-php-ext-configure gd \ + --with-freetype=/usr/include/ \ + --with-jpeg=/usr/include/; \ + docker-php-ext-install gd; \ + apk del .deps + +# PDO +RUN set -eux; \ + docker-php-ext-install pdo pdo_mysql + +# Sockets +# See: https://github.com/docker-library/php/issues/181#issuecomment-173365852 +RUN set -eux; \ + apk add --no-cache --virtual .deps linux-headers; \ + docker-php-ext-install sockets; \ + apk del .deps + +WORKDIR /web + +FROM base AS dev + +# Xdebug: https://stackoverflow.com/questions/46825502/how-do-i-install-xdebug-on-dockers-official-php-fpm-alpine-image +# PHPIZE_DEPS: autoconf dpkg-dev dpkg file g++ gcc libc-dev make pkgconf re2c +RUN apk add --no-cache --virtual .build-dependencies $PHPIZE_DEPS \ + && pecl install xdebug-3.1.6 \ + && docker-php-ext-enable xdebug \ + && docker-php-source delete \ + && apk del .build-dependencies +RUN { \ + echo "[xdebug]"; \ + echo "zend_extension=xdebug"; \ + echo "xdebug.mode=debug"; \ + echo "xdebug.start_with_request=yes"; \ + echo "xdebug.client_host=host.docker.internal"; \ + echo "xdebug.client_port=9000"; \ + } > /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini; + +RUN set -eux; \ + echo; \ + php -i; \ + php -m + +FROM base AS prod + +# Set permissions for 'www-data' user +# COPY --chown=www-data:www-data --chmod=640 /web /web +COPY ./web /web +RUN set -eux; \ + chown -R www-data:www-data /web; \ + find /web -type d -exec chmod 750 {} \; ; \ + find /web -type f -exec chmod 640 {} \; ; + +COPY ./heatmaps /heatmaps +RUN set -eux; \ + chown -R www-data:www-data /heatmaps; \ + find /heatmaps -type d -exec chmod 750 {} \; ; \ + find /heatmaps -type f -exec chmod 640 {} \; ; + +# Add default configs +COPY ./config/web/php/conf.d/php.ini /usr/local/etc/php/conf.d/php.ini +COPY ./config/web/php-fpm.d/www.conf /usr/local/etc/php-fpm.d/www.conf diff --git a/README.md b/README.md index 81996206..a5b7e183 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,8 @@ -## HLstatsX : Community Edition +# HLstatsX : Community Edition +[![github-actions](https://github.com/startersclan/hlstatsx-community-edition/workflows/ci-master-pr/badge.svg)](https://github.com/startersclan/hlstatsx-community-edition/actions) +[![github-release](https://img.shields.io/github/v/release/startersclan/hlstatsx-community-edition?style=flat-square)](https://github.com/startersclan/hlstatsx-community-edition/releases/) +[![docker-image-size](https://img.shields.io/docker/image-size/startersclan/hlstatsx-community-edition/asp-nginx)](https://hub.docker.com/r/startersclan/hlstatsx-community-edition) HLstatsX Community Edition is an open-source project licensed under GNU General Public License v2 and is a real-time stats @@ -8,17 +11,106 @@ Edition uses a Perl daemon to parse the log streamed from the game server. The data is stored in a MySQL Database and has a PHP frontend. +## :loudspeaker: Important changes -#### :loudspeaker: Important changes | Date | Description | Additional information | | ------------- | ------------- | ------------- | | 07.01.2020 | [#45](https://github.com/NomisCZ/hlstatsx-community-edition/issues/45) GeoIP2 Linux script updated, GeoLite2 MaxMind database (GDPR and CCPA) | https://blog.maxmind.com/2019/12/18/significant-changes-to-accessing-and-using-geolite2-databases/ | > Date format: DD.MM.YYYY + --- -### :book: Documentation -* https://github.com/NomisCZ/hlstatsx-community-edition/wiki 🚧 Wiki - work in progress 🚧 -### :speech_balloon: Help -* https://forums.alliedmods.net/forumdisplay.php?f=156 +## :book: Documentation + +- https://github.com/NomisCZ/hlstatsx-community-edition/wiki 🚧 Wiki - work in progress 🚧 + +## :speech_balloon: Help + +- https://forums.alliedmods.net/forumdisplay.php?f=156 + --- + +## Development + +```sh +# 1. Start Counter-strike 1.6 server, source-udp-forwarder, HLStatsX:CE stack +docker compose up +# HLStatsX:CE web frontend available at http://localhost:8081/. Admin Panel username: admin, password 123456 +# phpmyadmin available at http://localhost:8083. Root username: root, root password: root. Username: hlstatsxce, password: hlstatsxce + +# 2. Once setup, login to Admin Panel at http://localhost:8081/?mode=admin. Click HLstatsX:CE Settings > Proxy Settings, change the daemon's proxy key to 'somedaemonsecret' +# This enables gameserver logs forwarded via source-udp-forwarder to be accepted by the daemon. +# Then, restart the daemon. +docker compose restart daemon + +# 3. Finally, add a Counter-Strike 1.6 server. click Games > and unhide 'cstrike' game. +# Then, click Game Settings > Counter-Strike (cstrike) > Add Server. +# IP: 192.168.1.100 +# Port: 27015 +# Name: My Counter-Strike 1.6 server +# Rcon Password: password +# Public Address: example.com:27015 +# Admin Mod: AMX Mod X +# On the next page, click Apply. + +# 4. Reload the daemon via Tools > HLstatsX: CE Daemon Control, using Daemon IP: daemon, port: 27500. You should see the daemon reloaded in the logs. +# The stats of the gameserver is now recorded :) + +# 5. To verify stats recording works, restart the gameserver. You should see the daemon recording the gameserver logs. All the best :) +docker compose restart cstrike + +# Development - Install vscode extensions +# Once installed, set breakpoints in code, and press F5 to start debugging. +code --install-extension bmewburn.vscode-intelephense-client # PHP intellisense +code --install-extension xdebug.php-debug # PHP remote debugging via xdebug +# If xdebug is not working, iptables INPUT chain may be set to DROP on the docker bridge. +# Execute this to allow php to reach the host machine via the docker0 bridge +sudo iptables -A INPUT -i br+ -j ACCEPT + +# CS 1.6 server - Restart server +docker compose restart cstrike +# CS 1.6 server - Attach to the CS 1.6 server console. Press CTRL+P and then CTRL+Q to detach +docker attach $( docker compose ps -q cstrike ) +# CS 1.6 server - Exec into container +docker exec -it $( docker compose ps -q cstrike) bash + +# web-nginx - Exec into container +docker exec -it $( docker compose ps -q web-nginx ) sh +# web-php - Exec into container +docker exec -it $( docker compose ps -q web-php ) sh +# Run awards +docker exec -it $( docker compose ps -q awards) sh -c /awards.sh +# Generate heatmaps +docker exec -it $( docker compose ps -q heatmaps) php /heatmaps/generate.php #--disable-cache=true +# db - Exec into container +docker exec -it $( docker compose ps -q db ) sh + +# Test routes +docker compose -f docker compose.test.yml up + +# Test production builds locally +docker build -t startersclan/hlstatsx-community-edition:daemon -f Dockerfile.daemon . +docker build -t startersclan/hlstatsx-community-edition:web-nginx -f Dockerfile.web-nginx . +docker build -t startersclan/hlstatsx-community-edition:web-php -f Dockerfile.web-php . + +# Dump the DB +docker exec $( docker compose ps -q db ) mysqldump -uroot -proot hlstatsxce | gzip > hlstatsxce.sql.gz + +# Restore the DB +zcat hlstatsxce.sql.gz | docker exec -i $( docker compose ps -q db ) mysql -uroot -proot hlstatsxce + +# Stop Counter-strike 1.6 server, source-udp-forwarder, HLStatsX:CE stack +docker compose down + +# Cleanup +docker compose down +docker volume rm hlstatsx-community-edition-dns-volume +docker volume rm hlstatsx-community-edition-db-volume +``` + +## FAQ + +### Q: `Xdebug: [Step Debug] Could not connect to debugging client. Tried: host.docker.internal:9000 (through xdebug.client_host/xdebug.client_port)` appears in the php logs + +A: The debugger is not running. Press `F5` in `vscode` to start the `php` `xdebug` debugger. If you stopped the debugger, it is safe to ignore this message. diff --git a/config/db/my.cnf b/config/db/my.cnf new file mode 100644 index 00000000..42e67bad --- /dev/null +++ b/config/db/my.cnf @@ -0,0 +1,43 @@ +[client] +port=3306 +default_character_set=utf8mb4 + +[mysql] +default_character_set=utf8mb4 + +[mysqldump] +default_character_set=utf8mb4 +quick +quote_names +max_allowed_packet = 16M + +[mysqld] +skip_name_resolve +character_set_client=utf8mb4 +character_set_server=utf8mb4 +collation_server=utf8mb4_unicode_ci +local_infile=0 +innodb_strict_mode +innodb_file_per_table +# Size of each log file in a log group. You should set the combined size +# of log files to about 25%-100% of your buffer pool size to avoid +# unneeded buffer pool flush activity on log file overwrite. However, +# note that a larger logfile size will increase the time needed for the +# recovery process. +innodb_log_file_size=8M + +# The size of the buffer InnoDB uses for buffering log data. As soon as +# it is full, InnoDB will have to flush it to disk. As it is flushed +# once per second anyway, it does not make sense to have it very large +# (even with long transactions). +innodb_log_buffer_size=8M + +# InnoDB, unlike MyISAM, uses a buffer pool to cache both indexes and +# row data. The bigger you set this the less disk I/O is needed to +# access data in tables. On a dedicated database server you may set this +# parameter up to 80% of the machine physical memory size. Do not set it +# too large, though, because competition of the physical memory may +# cause paging in the operating system. Note that on 32bit systems you +# might be limited to 2-3.5G of user level memory per process, so do not +# set it too high. +innodb_buffer_pool_size=32M diff --git a/config/heatmaps/config.inc.php b/config/heatmaps/config.inc.php new file mode 100755 index 00000000..ef93e0af --- /dev/null +++ b/config/heatmaps/config.inc.php @@ -0,0 +1,20 @@ + diff --git a/config/web/nginx/nginx.conf b/config/web/nginx/nginx.conf new file mode 100644 index 00000000..2000f627 --- /dev/null +++ b/config/web/nginx/nginx.conf @@ -0,0 +1,103 @@ +user nginx; +worker_processes 1; + +error_log /var/log/nginx/error.log warn; +pid /var/run/nginx.pid; + + +events { + worker_connections 1024; +} + + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + + sendfile on; + #tcp_nopush on; + + keepalive_timeout 65; + + #gzip on; + + server { + listen 80; + # Ensure our redirects don't go to other ports, see https://serverfault.com/questions/351212/nginx-redirects-to-port-8080-when-accessing-url-without-slash + port_in_redirect off; + # Ensure our redirects are scheme-agnostic. Important behind a SSL-terminated load balancer. + absolute_redirect off; + + root /web; + + access_log /var/log/nginx/access.log; + error_log /var/log/nginx/error.log; + + index index.php index.html index.htm; + + # Increase upload max body size + client_max_body_size 5m; + client_body_buffer_size 5m; + + # Restore IP from Proxy + set_real_ip_from 127.0.0.0/8; + set_real_ip_from 10.0.0.0/8; + set_real_ip_from 172.16.0.0/12; + set_real_ip_from 192.168.0.0/16; + real_ip_header X-Real-IP; + + # We don't want to pass calls to css, script, and image files to the index, + # whether they exist or not. So quit right here, and allow direct access + # to common format files. Add formats here to allow direct link access + location ~ \.(gif|png|jpe?g|bmp|css|js|swf|wav|avi|mpg|ttf|woff|ico)$ { + # Show empty flag + # location ~ (/web/hlstatsimg/images/flags)/.*\.(png|jpeg|jpg|gif)$ { + # try_files $uri $1/xx.png =404; + # } + + access_log off; + add_header Cache-Control "public, s-maxage=600, maxage=600"; + try_files $uri =404; + } + + # Deny access to hidden files + location ~ /\.[^/]+$ { + return 401; + } + # Deny access to the certain folders + location ~ ^/(includes|pages|updater|system) { + return 401; + } + + location ~ \.php$ { + # Check that the PHP script exists before passing it + try_files $fastcgi_script_name =404; + + # Disable fastcgi output buffering + fastcgi_buffering off; + + # Set fastcgi max execution response time + fastcgi_read_timeout 3600s; + fastcgi_split_path_info ^(.+\.php)(/.+)$; + include fastcgi_params; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + fastcgi_index index.php; + fastcgi_pass web-php:9000; + } + + location / { + limit_except GET POST { + deny all; + } + + # Pass everything else to php + try_files $uri $uri/ =404; + } + } +} diff --git a/config/web/php-fpm.d/www.conf b/config/web/php-fpm.d/www.conf new file mode 100644 index 00000000..eefbe39b --- /dev/null +++ b/config/web/php-fpm.d/www.conf @@ -0,0 +1,10 @@ +[www] +user = www-data +group = www-data +security.limit_extensions = .php .aspx +pm = dynamic +pm.max_children = 5 +pm.start_servers = 2 +pm.min_spare_servers = 1 +pm.max_spare_servers = 3 +pm.status_path = /status.php diff --git a/config/web/php/conf.d/php.ini b/config/web/php/conf.d/php.ini new file mode 100644 index 00000000..16be36b5 --- /dev/null +++ b/config/web/php/conf.d/php.ini @@ -0,0 +1,27 @@ +[hardening] +disable_functions = exec,passthru,shell_exec,system,proc_open,popen,parse_ini_file +cgi.fix_pathinfo = 0 +cgi.force_redirect = 1 +allow_url_fopen = 0 +allow_url_include = 0 +expose_php = 0 + +open_basedir = /web:/heatmaps:/tmp +upload_tmp_dir = /tmp + +display_errors = 0 +log_errors = 1 +error_reporting = E_ALL +ignore_repeated_errors = 1 +#error_log = /var/lib/php/logs/error.log + +max_execution_time = 35 +max_input_time = 35 +file_uploads = 1 +post_max_size = 5M +upload_max_filesize = 5M + +[opcache] +opcache.enable=1 +opcache.memory_consumption = 64 +opcache.fast_shutdown = 0 diff --git a/docker-compose.test.yml b/docker-compose.test.yml new file mode 100644 index 00000000..a0620a49 --- /dev/null +++ b/docker-compose.test.yml @@ -0,0 +1,58 @@ +version: '2.2' +services: + test-routes: + image: alpine:latest + environment: + URLS: | + http://web-nginx/ 302 + http://web-nginx/css/spinner.gif 200 + http://web-nginx/hlstatsimg/ajax.gif 200 + http://web-nginx/includes/ 401 + http://web-nginx/pages/ 401 + http://web-nginx/pages/.htaccess 401 + http://web-nginx/styles/classic.css 200 + http://web-nginx/updater/ 401 + http://web-nginx/autocomplete.php 200 + http://web-nginx/config.php 200 + http://web-nginx/hlstats.php 200 + http://web-nginx/index.php 302 + http://web-nginx/ingame.php 200 + http://web-nginx/show_graph.php 200 + http://web-nginx/sig.php 200 + http://web-nginx/status.php 200 + http://web-nginx/trend_graph.php 200 + networks: + - default + entrypoint: + - /bin/sh + command: + - -c + - | + set -eu + + echo "Waiting for stack to be ready" + s=0 + while true; do + nc -vz -w 1 web-nginx 80 \ + && nc -vz -w 1 web-php 9000 \ + && nc -vz -w 1 db 3306 \ + && break || true + s=$$(( $$s + 1 )) + if [ "$$s" -eq 600 ]; then + exit 1 + fi + echo "Retrying in 3 seconds" + sleep 3 + done + + echo "$$URLS" | awk NF | while read -r i j; do + if wget -q -SO- "$$i" 2>&1 | grep "HTTP/1.1 $$j " > /dev/null; then + echo "PASS: $$i" + else + echo "FAIL: $$i" + exit 1 + fi + done + +networks: + default: diff --git a/docker-compose.tmp.yml b/docker-compose.tmp.yml new file mode 100644 index 00000000..e3c26e97 --- /dev/null +++ b/docker-compose.tmp.yml @@ -0,0 +1,12 @@ +version: '2.2' +services: + test: + build: + dockerfile_inline: | + FROM alpine:3.17 + RUN echo hello + cache_from: + - type=local,src=/tmp/.buildx-cache + cache_to: + - type=local,dest=/tmp/.buildx-cache,mode=max + diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..ddd33028 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,263 @@ +version: '2.2' +services: + # 1. Counter-Strike 1.6 gameserver sends UDP logs to source-udp-forwarder + # See: https://github.com/startersclan/docker-sourceservers + cstrike: + image: goldsourceservers/cstrike:latest + volumes: + - dns-volume:/dns:ro + ports: + - 27015:27015/udp + networks: + - default + stdin_open: true + tty: true + stop_signal: SIGKILL + depends_on: + - source-udp-forwarder + entrypoint: + - /bin/bash + command: + - -c + - | + set -eu + exec hlds_linux -console -noipx -secure -game cstrike +map de_dust2 +maxplayers 32 +sv_lan 0 +ip 0.0.0.0 +port 27015 +rcon_password password +log on +logaddress_add "$$( cat /dns/source-udp-forwarder )" 26999 + + # 2. source-udp-forwarder proxy forwards gameserver logs to the daemon + # See: https://github.com/startersclan/source-udp-forwarder + source-udp-forwarder: + image: startersclan/source-udp-forwarder:latest + environment: + - UDP_LISTEN_ADDR=:26999 + - UDP_FORWARD_ADDR=daemon:27500 + - FORWARD_PROXY_KEY=somedaemonsecret # The daemon's proxy_key secret + - FORWARD_GAMESERVER_IP=192.168.1.100 # The gameserver's IP as registered in the HLStatsX:CE database + - FORWARD_GAMESERVER_PORT=27015 # The gameserver's IP as registered in the HLStatsX:CE database + - LOG_LEVEL=INFO + - LOG_FORMAT=txt + volumes: + - dns-volume:/dns + networks: + - default + depends_on: + - daemon + entrypoint: + - /bin/sh + command: + - -c + - | + set -eu + + echo "Outputting my IP address" + ip addr show eth0 | grep 'inet ' | awk '{print $$2}' | cut -d '/' -f1 | tee /dns/source-udp-forwarder + + exec /source-udp-forwarder + + # 3. HLStatsX:CE perl daemon accepts the gameserver logs. Gameserver Logs are parsed and stats are recorded + # The daemon's proxy_key secret can only be setup in the HLStatsX:CE Web Admin Panel Settings under 'Proxy Settings' section + # HLStatsX:CE perl daemon: https://github.com/startersclan/docker-hlstatsxce-daemon + # NOTE: Currently, as of v1.6.19, the daemon crashes upon startup. You will need to fix perl errors and rebuild the image. + daemon: + build: + dockerfile: Dockerfile.daemon + context: . + target: dev + cache_from: + - type=local,src=/tmp/.buildx-cache + cache_to: + - type=local,dest=/tmp/.buildx-cache,mode=max + ports: + - 27500:27500/udp # For external servers to send logs to the daemon + networks: + - default + command: + - perl + - hlstats.pl + - --ip=0.0.0.0 + - --port=27500 + - --db-host=db:3306 + - --db-name=hlstatsxce + - --db-username=hlstatsxce + - --db-password=hlstatsxce + - --nodns-resolveip + - --rcon + - --debug + # - --debug + # - --help + + # Cron - awards + awards: + build: + dockerfile: Dockerfile.daemon + context: . + target: dev + cache_from: + - type=local,src=/tmp/.buildx-cache + cache_to: + - type=local,dest=/tmp/.buildx-cache,mode=max + stop_signal: SIGKILL + entrypoint: + - /bin/sh + command: + - -c + - | + set -eu + + echo "Creating /awards.sh" + cat - > /awards.sh <<'EOF' + #!/bin/sh + set -eu + cd /scripts + perl hlstats-awards.pl --db-host=db:3306 --db-name=hlstatsxce --db-username=hlstatsxce --db-password=hlstatsxce #--help + EOF + chmod +x /awards.sh + + # Run at 00:00 daily. To customize your cron schedule, use https://crontab.guru + echo "Creating crontab" + crontab - <<'EOF' + PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin + 0 0 * * * /awards.sh > /proc/1/fd/1 2>/proc/1/fd/2 + EOF + crontab -l + + echo "Running cron" + cron -f + + # 4. HLStatsX:CE DB + db: + image: mysql:5.7 + environment: + - MYSQL_ROOT_PASSWORD=root # Username 'root', password 'root' + - MYSQL_USER=hlstatsxce + - MYSQL_PASSWORD=hlstatsxce + - MYSQL_DATABASE=hlstatsxce + volumes: + - db-volume:/var/lib/mysql + - ./sql/install.sql:/docker-entrypoint-initdb.d/install.sql:ro + networks: + - default + + # 5a. HLStatsX:CE web - nginx + # Available at http://localhost:8083 + # Admin Panel username: admin, password: 123456 + web-nginx: + build: + dockerfile: Dockerfile.web-nginx + context: . + target: dev + cache_from: + - type=local,src=/tmp/.buildx-cache + cache_to: + - type=local,dest=/tmp/.buildx-cache,mode=max + volumes: + - ./web:/web + - ./config/web/nginx/nginx.conf:/etc/nginx/nginx.conf:ro + ports: + - 8081:80 + networks: + - default + depends_on: + - init-container + - web-php + working_dir: /web + + # 5b. HLStatsX:CE web - php + web-php: + build: + dockerfile: Dockerfile.web-php + context: . + target: dev + cache_from: + - type=local,src=/tmp/.buildx-cache + cache_to: + - type=local,dest=/tmp/.buildx-cache,mode=max + volumes: + - ./web:/web + - ./config/web/config.php:/web/config.php:ro # Main config file. + - ./config/web/php/conf.d/php.ini:/usr/local/etc/php/conf.d/php.ini:ro + - ./config/web/php-fpm.d/www.conf:/usr/local/etc/php-fpm.d/www.conf:ro + networks: + - default + extra_hosts: + # For xdebug to reach the host via `host.docker.internal`. See: https://github.com/moby/moby/pull/40007#issuecomment-578729356 and https://stackoverflow.com/questions/49907308/installing-xdebug-in-docker + # If xdebug does not work, you may need to add an iptables rule to the INPUT chain: iptables -A INPUT -i br+ -j ACCEPT + - host.docker.internal:host-gateway + depends_on: + - init-container + + # Cron - Heatmaps + heatmaps: + build: + dockerfile: Dockerfile.web-php + context: . + target: base + cache_from: + - type=local,src=/tmp/.buildx-cache + cache_to: + - type=local,dest=/tmp/.buildx-cache,mode=max + volumes: + - ./web:/web + - ./heatmaps:/heatmaps + - ./config/heatmaps/config.inc.php:/heatmaps/config.inc.php:ro # Heatmaps config file. + working_dir: /heatmaps + stop_signal: SIGKILL + entrypoint: + - /bin/sh + command: + - -c + - | + set -eu + + # Run at 00:00 daily. To customize your cron schedule, use https://crontab.guru + echo "Creating crontab" + crontab - <<'EOF' + 0 0 * * * php /heatmaps/generate.php > /proc/1/fd/1 2>/proc/1/fd/2 + EOF + crontab -l + + echo "Running crond" + exec crond -f + + # PHPMyAdmin to manage DB + # Available at http://localhost:8083 + phpmyadmin: + image: phpmyadmin:5.2 + environment: + - PMA_HOST=db + ports: + - 8083:80 + networks: + - default + + # Init container to set permissions in mounted folders and volumes + init-container: + image: alpine:latest + volumes: + - ./web:/web + - db-volume:/var/lib/mysql + networks: + - default + entrypoint: + - /bin/sh + command: + - -c + - | + set -eu + + echo "Granting web nginx and php read permissions" + find /web -type d -exec chmod 755 {} \; + find /web -type f -exec chmod 644 {} \; + + echo "Granting web php write permissions" + chmod 777 /web/hlstatsimg/progress + chmod 777 /web/hlstatsimg/graph + + echo "Granting db write permissions" + chown -R 999:999 /var/lib/mysql + +networks: + default: + +volumes: + dns-volume: + db-volume: diff --git a/scripts/HLstats.plib b/scripts/HLstats.plib index 23a626d2..268e780a 100644 --- a/scripts/HLstats.plib +++ b/scripts/HLstats.plib @@ -2,11 +2,11 @@ # Copyleft (L) 2008-20XX Nicholas Hastings (nshastings@gmail.com) # http://www.hlxcommunity.com # -# HLstatsX Community Edition is a continuation of +# HLstatsX Community Edition is a continuation of # ELstatsNEO - Real-time player and clan rankings and statistics # Copyleft (L) 2008-20XX Malte Bayer (steam@neo-soft.org) # http://ovrsized.neo-soft.org/ -# +# # ELstatsNEO is an very improved & enhanced - so called Ultra-Humongus Edition of HLstatsX # HLstatsX - Real-time player and clan rankings and statistics for Half-Life 2 # http://www.hlstatsx.com/ @@ -16,7 +16,7 @@ # HLstats - Real-time player and clan rankings and statistics for Half-Life # http://sourceforge.net/projects/hlstats/ # Copyright (C) 2001 Simon Garner -# +# # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 @@ -30,7 +30,7 @@ # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. -# +# # For support and installation notes visit http://www.hlxcommunity.com @@ -95,12 +95,12 @@ sub number_format { sub date_format { my $timestamp = shift; - return sprintf('%dd %02d:%02d:%02dh', - $timestamp / 86400, - $timestamp / 3600 % 24, - $timestamp / 60 % 60, - $timestamp % 60 - ); + return sprintf('%dd %02d:%02d:%02dh', + $timestamp / 86400, + $timestamp / 3600 % 24, + $timestamp / 60 % 60, + $timestamp % 60 + ); } @@ -114,7 +114,7 @@ sub date_format { sub error { my $errormsg = $_[0]; - + if ($g_mailto && $g_mailpath) { system("echo \"$errormsg\" | $g_mailpath -s \"HLstatsX:CE crashed `date`\" $g_mailto"); @@ -137,7 +137,7 @@ sub quoteSQL $varQuote =~ s/\\/\\\\/g; # replace \ with \\ $varQuote =~ s/'/\\'/g; # replace ' with \' - + return $varQuote; } @@ -154,7 +154,7 @@ sub doConnect $db_user, $db_pass, { mysql_enable_utf8 => 1 } ); while(!$db_conn) { - &printEvent("MYSQL", "\nCan't connect to MySQL database '$db_name' on '$db_host'\n" . + &printEvent("MYSQL", "\nCan't connect to MySQL database '$db_name' on '$db_host' and user '$db_user' and pass '$db_pass'\n" . "Server error: $DBI::errstr\n"); sleep(5); $db_conn = DBI->connect( @@ -183,7 +183,7 @@ sub doQuery my $result = $db_conn->prepare($query) or die("Unable to prepare query:\n$query\n$DBI::errstr\n$callref"); $result->execute or die("Unable to execute query:\n$query\n$DBI::errstr\n$callref"); - + return $result; } @@ -200,16 +200,16 @@ sub execNonQuery sub execCached { my ($query_id,$query, @bind_args) = @_; - + if(!$db_conn->ping()) { &printEvent("HLSTATSX", "Lost database connection. Trying to reconnect...", 1); &doConnect(); } - + if(!$db_stmt_cache{$query_id}) { $db_stmt_cache{$query_id} = $db_conn->prepare($query) or die("Unable to prepare query ($query_id):\n$query\n$DBI::errstr"); #&printEvent("HLSTATSX", "Prepared a statement ($query_id) for the first time.", 1); - } + } $db_stmt_cache{$query_id}->execute(@bind_args) or die ("Unable to execute query ($query_id):\n$query\n$DBI::errstr"); return $db_stmt_cache{$query_id}; } @@ -225,13 +225,13 @@ sub resolveIp { my ($ip, $quiet) = @_; my ($host) = ""; - + unless ($g_dns_resolveip) { return ""; } - - + + eval { $SIG{ALRM} = sub { die "DNS Timeout\n" }; @@ -239,7 +239,7 @@ sub resolveIp $host = gethostbyaddr(inet_aton($ip), AF_INET); alarm 0; }; - + if ($@) { my $error = $@; @@ -292,13 +292,13 @@ sub getHostGroup { my ($hostname, $result) = @_; my $hostgroup = ""; - + # User can define special named hostgroups in hlstats_HostGroups, i.e. # '.adsl.someisp.net' => 'SomeISP ADSL' - + $result = &queryHostGroups() unless ($result); $result->execute(); - + while (my($pattern, $name) = $result->fetchrow_array()) { $pattern = quotemeta($pattern); @@ -310,7 +310,7 @@ sub getHostGroup } } $result->finish; - + if (!$hostgroup) { # @@ -325,7 +325,7 @@ sub getHostGroup # # Please mail sgarner@hlstats.org with any additions. # - + my @dom_nosld = ( "ca", # Canada "ch", # Switzerland @@ -341,9 +341,9 @@ sub getHostGroup "ru", # Russia "se", # Sweden ); - + my $dom_nosld = join("|", @dom_nosld); - + if ($hostname =~ /([\w-]+\.(?:$dom_nosld|\w\w\w))$/) { $hostgroup = $1; @@ -357,7 +357,7 @@ sub getHostGroup $hostgroup = $hostname; } } - + return $hostgroup; } @@ -371,7 +371,7 @@ sub getHostGroup sub doConf { my ($conf, %directives) = @_; - + while (($directive, $variable) = each(%directives)) { if ($directive eq "Servers") { @@ -392,7 +392,7 @@ sub doConf sub setOptionsConf { my (%optionsconf) = @_; - + while (($thekey, $theval) = each(%optionsconf)) { if($theval) @@ -414,9 +414,9 @@ sub setOptionsConf sub abbreviate { my ($thestring, $maxlength) = @_; - + $maxlength = 12 unless ($maxlength); - + if (length($thestring) > $maxlength) { $thestring = substr($thestring, 0, $maxlength - 3); @@ -442,13 +442,13 @@ sub printEvent my ($sec,$min,$hour,$mday,$mon,$year) = localtime(time()); my $timestamp = sprintf("%04d-%02d-%02d %02d:%02d:%02d", $year+1900, $mon+1, $mday, $hour, $min, $sec); if ($update_timestamp == 0) { - $timestamp = $ev_timestamp; - } + $timestamp = $ev_timestamp; + } if (is_number($code)) { printf("%s: %21s - E%03d: %s\n", $timestamp, $s_addr, $code, $description); } else { printf("%s: %21s - %s: %s\n", $timestamp, $s_addr, $code, $description); - } + } } } diff --git a/web/hlstats.php b/web/hlstats.php index beb56c5c..637f25fc 100644 --- a/web/hlstats.php +++ b/web/hlstats.php @@ -203,17 +203,20 @@ 'profile' ); -if (file_exists('./updater') && $mode != 'updater') -{ - pageHeader(array('Update Notice'), array('Update Notice' => '')); - echo "
\n" . - "\"Warning\" Warning:
\n" . - "The updater folder was detected in your web directory.
- To perform a Database Update, please go to HLX:CE Database Updater to perform the database update.

- If you have already performed the database update, you must delete the \"updater\" folder from your web folder.
\n
"; - pageFooter(); - die(); -} +// In docker, the updater folder will always be present, to allow +// DB upgrades to be done using this updater. Hence, this code is +// commented out to allow things to work correctly after the DB upgrade in docker. +// if (file_exists('./updater') && $mode != 'updater') +// { +// pageHeader(array('Update Notice'), array('Update Notice' => '')); +// echo "
\n" . +// "\"Warning\" Warning:
\n" . +// "The updater folder was detected in your web directory.
+// To perform a Database Update, please go to HLX:CE Database Updater to perform the database update.

+// If you have already performed the database update, you must delete the \"updater\" folder from your web folder.
\n
"; +// pageFooter(); +// die(); +// } if ( !in_array($mode, $valid_modes) ) {