From 1080424e4ce3077bbc0c55f6edeca7ceae32d8b3 Mon Sep 17 00:00:00 2001 From: Patrick Asmus Date: Wed, 21 Oct 2026 20:55:55 +0200 Subject: [PATCH] feat: webtrees docker build setup with patched PHP 8.4.26 (CVE-2026-109618 / CVE-2026-109619) --- .htaccess | 19 ++ Dockerfile | 78 +++++ README.md | 116 ++++++- apache/webtrees-redir.conf | 13 + apache/webtrees-ssl.conf | 14 + apache/webtrees.conf | 10 + docker-entrypoint.py | 602 ++++++++++++++++++++++++++++++++++ docker-healthcheck.sh | 12 + patches/UpgradeService1.patch | 39 +++ patches/UpgradeService2.patch | 39 +++ 10 files changed, 937 insertions(+), 5 deletions(-) create mode 100644 .htaccess create mode 100644 Dockerfile create mode 100644 apache/webtrees-redir.conf create mode 100644 apache/webtrees-ssl.conf create mode 100644 apache/webtrees.conf create mode 100644 docker-entrypoint.py create mode 100644 docker-healthcheck.sh create mode 100644 patches/UpgradeService1.patch create mode 100644 patches/UpgradeService2.patch diff --git a/.htaccess b/.htaccess new file mode 100644 index 0000000..31155ed --- /dev/null +++ b/.htaccess @@ -0,0 +1,19 @@ +RewriteEngine On + +# https://webtrees.net/faq/urls/ +# GIT config files can contain credentials or other sensitive data. +RewriteRule \.git - [F] + +# User data is stored here by default. +RewriteRule ^data(/|$) - [F] + +# Nothing sensitive here, but there is no need to publish it. +RewriteRule ^app(/|$) - [F] +RewriteRule ^modules - [F] +RewriteRule ^resources(/|$) - [F] +RewriteRule ^vendor(/|$) - [F] + +RewriteCond %{REQUEST_FILENAME} !-d +RewriteCond %{REQUEST_FILENAME} !-f + +RewriteRule ^ index.php [L] \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..1d56d5f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,78 @@ +ARG PHP_VERSION=8.4 + +FROM docker.io/library/php:$PHP_VERSION-apache + +# https://hub.docker.com/_/php +# https://github.com/NathanVaughn/webtrees-docker/issues/160 +RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini" + +ENV WEBTREES_HOME="/var/www/webtrees" +WORKDIR $WEBTREES_HOME + +# install pre-reqs +# postgresql-client provides pg_isready +# mariadb-client provides mysqladmin +RUN apt-get update \ + && apt-get install -y \ + curl \ + libmagickwand-dev \ + libpq-dev \ + libzip-dev \ + postgresql-client \ + mariadb-client \ + patch \ + python3 \ + unzip \ + --no-install-recommends \ + && rm -rf /var/lib/apt/lists/* + +# install php extensions +RUN pecl install imagick \ + && docker-php-ext-enable imagick \ + && docker-php-ext-configure gd --with-freetype --with-jpeg \ + && docker-php-ext-install -j"$(nproc)" pdo pdo_mysql pdo_pgsql zip intl gd exif opcache + +# remove old apt stuff +RUN apt-get purge gcc g++ make -y \ + && apt-get autoremove -y \ + && apt-get clean \ + && rm -rf /var/tmp/* /etc/apache2/sites-enabled/000-*.conf + +ARG WEBTREES_VERSION +RUN curl -s -L https://github.com/fisharebest/webtrees/releases/download/${WEBTREES_VERSION}/webtrees-${WEBTREES_VERSION}.zip -o webtrees.zip \ + && unzip -q webtrees.zip -d /var/www/ && rm webtrees.zip \ + && rm $WEBTREES_HOME/*.md + +# Disable version update prompt. Webtrees should not be upgrading itself, +# users should be using tagged container versions +ARG UPGRADE_PATCH_VERSION +COPY patches/UpgradeService${UPGRADE_PATCH_VERSION}.patch /UpgradeService.patch +RUN patch app/Services/UpgradeService.php /UpgradeService.patch \ + && rm /UpgradeService.patch \ +# Delete file that caused email issues +# https://www.webtrees.net/index.php/fr/forum/help-for-2-0/36616-email-error-after-update-to-2-0-21#89985 +# https://github.com/NathanVaughn/webtrees-docker/issues/88 + && rm vendor/egulias/email-validator/src/Validation/MessageIDValidation.php + +# enable apache modules +RUN a2enmod rewrite && a2enmod ssl && rm -rf /var/www/html + +# copy apache/php configs +RUN mkdir -p /bak/webtrees/data/ \ + && cp $WEBTREES_HOME/data/.htaccess /bak/webtrees/data/.htaccess +COPY .htaccess ./ +COPY apache/ /etc/apache2/sites-available/ + +# entrypoint +COPY docker-entrypoint.py / + +# healthcheck +COPY docker-healthcheck.sh / +RUN chmod +x /docker-healthcheck.sh + +# final Docker config +EXPOSE 80 443 +VOLUME ["$WEBTREES_HOME/data"] + +HEALTHCHECK CMD /docker-healthcheck.sh +ENTRYPOINT ["python3", "/docker-entrypoint.py"] diff --git a/README.md b/README.md index dc14603..d0256a0 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,10 @@

-

Name des Projekts

+

Webtrees Docker (PHP 8.4 Security Patched)

- Kurzbeschreibung des Projekts/Anwendung, um die es geht + Webtrees Docker Image mit gepatchtem PHP 8.4.26 (Schutz vor CVE-2026-109618 / CVE-2026-109619 "PHP2Shell")

@@ -21,9 +21,115 @@


+## 📌 Übersicht & Hintergrund -CONTENT BEREICH +Dieses Repository stellt vorgefertigte Docker-Images für die führende Open-Source-Genealogie-Software **[webtrees](https://webtrees.net/)** bereit. +### 🛡️ Warum dieser Fork / Rebuild? +Am 21. Oktober 2026 wurde die kritische Zero-Day-Schwachstelle **"PHP2Shell"** ([CVE-2026-109618](https://nvd.nist.gov/vuln/detail/CVE-2026-109618) & CVE-2026-109619, CVSS 9.2) im PHP-Kern bekannt. + +Da bisherige INI-Workarounds (`session.upload_progress.enabled = Off`) laut Sicherheitsberichten (u. a. Heise Security) unvollständig sind bzw. umgangen werden können, ist der Einsatz der offiziell gepatchten PHP-Binaries zwingend erforderlich. + +Da das beliebte Upstream-Image von [NathanVaughn/webtrees-docker](https://github.com/NathanVaughn/webtrees-docker) nur bei neuen Webtrees-Releases automatisiert baut, wurde dieses Image direkt auf Basis des heute veröffentlichten Upstream-Patches **`php:8.4-apache` (PHP 8.4.26, Build: 21.10.2026 00:32:44 UTC)** gebaut und getestet. + +* **Upstream-Basis:** 1:1 kompatibel zu [NathanVaughn/webtrees-docker](https://github.com/NathanVaughn/webtrees-docker) +* **Webtrees-Versionen:** `2.2.6` und `2.2.1` +* **PHP-Version:** `PHP 8.4.26` (Offizielles Security-Patched Release vom 21.10.2026) +* **Enthaltene Module:** `imagick`, `gd`, `pdo_mysql`, `pdo_pgsql`, `zip`, `intl`, `exif`, `opcache` uvm. + +--- + +## 📦 Verfügbare Container Tags + +Die Images können direkt aus der Techniverse Gitea Container Registry bezogen werden: + +```bash +# Webtrees 2.2.6 (PHP 8.4.26) +docker pull git.techniverse.net/scriptos/webtrees:2.2.6-php8.4.26 +docker pull git.techniverse.net/scriptos/webtrees:latest-php8.4.26 + +# Webtrees 2.2.1 (PHP 8.4.26) +docker pull git.techniverse.net/scriptos/webtrees:2.2.1-php8.4.26 +``` + +--- + +## 🚀 Schnellstart mit Docker Compose + +Beispiel für eine `docker-compose.yaml`: + +```yaml +services: + webtrees-app: + image: git.techniverse.net/scriptos/webtrees:2.2.6-php8.4.26 + container_name: webtrees-app + restart: unless-stopped + depends_on: + - webtrees-db + environment: + PRETTY_URLS: "1" + HTTPS: "0" + HTTPS_REDIRECT: "0" + LANG: "de" + TZ: "Europe/Berlin" + BASE_URL: "https://stammbaum.deine-domain.de" + DB_TYPE: "mysql" + DB_HOST: "webtrees-db" + DB_PORT: "3306" + DB_USER: "webtrees" + DB_PASS: "DeinSicheresPasswort" + DB_NAME: "webtrees" + DB_PREFIX: "wt_" + PUID: "33" + PGID: "33" + PHP_UPLOAD_MAX_FILE_SIZE: "50M" + PHP_POST_MAX_SIZE: "50M" + PHP_MEMORY_LIMIT: "1024M" + ports: + - "8080:80" + volumes: + - ./data/app_data:/var/www/webtrees/data/ + - ./data/modules_v4:/var/www/webtrees/modules_v4 + + webtrees-db: + image: mariadb:11 + container_name: webtrees-db + restart: unless-stopped + environment: + MARIADB_DATABASE: "webtrees" + MARIADB_USER: "webtrees" + MARIADB_PASSWORD: "DeinSicheresPasswort" + MARIADB_ROOT_PASSWORD: "DeinRootPasswort" + volumes: + - ./data/db_data:/var/lib/mysql +``` + +--- + +## 🔍 Verifikation des Sicherheits-Patches + +Im laufenden Container lässt sich der Patch-Stand jederzeit über die PHP-CLI prüfen: + +```bash +docker exec webtrees-app php -v +``` + +Ausgabe: +```text +PHP 8.4.26 (cli) (built: Oct 21 2026 00:32:44) (NTS) +Copyright (c) The PHP Group +Built by https://github.com/docker-library/php +Zend Engine v4.4.26, Copyright (c) Zend Technologies + with Zend OPcache v8.4.26, Copyright (c), by Zend Technologies +``` + +--- + +## 🔗 Danksagung & Upstream-Referenzen + +* **webtrees:** Offizielles Projekt unter [https://webtrees.net/](https://webtrees.net/) und auf [GitHub (fisharebest/webtrees)](https://github.com/fisharebest/webtrees) +* **Docker Image Vorlage:** Nathan Vaughn ([NathanVaughn/webtrees-docker](https://github.com/NathanVaughn/webtrees-docker)) +* **PHP Security:** PHP Group ([php.net](https://www.php.net/))

@@ -32,6 +138,6 @@ CONTENT BEREICH

- © Patrick Asmus · Techniverse Network · Lizenz + © Patrick Asmus · Techniverse Network · GPL-3.0 Lizenz -

\ No newline at end of file +

diff --git a/apache/webtrees-redir.conf b/apache/webtrees-redir.conf new file mode 100644 index 0000000..450bae8 --- /dev/null +++ b/apache/webtrees-redir.conf @@ -0,0 +1,13 @@ + + ServerName webtrees + DocumentRoot "/var/www/webtrees/" + + RewriteEngine On + RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [END,NE,R=permanent] + + + Options FollowSymLinks MultiViews + AllowOverride All + Require all granted + + \ No newline at end of file diff --git a/apache/webtrees-ssl.conf b/apache/webtrees-ssl.conf new file mode 100644 index 0000000..8b496cd --- /dev/null +++ b/apache/webtrees-ssl.conf @@ -0,0 +1,14 @@ + + ServerName webtrees + DocumentRoot "/var/www/webtrees/" + + SSLEngine on + SSLCertificateFile /certs/webtrees.crt + SSLCertificateKeyFile /certs/webtrees.key + + + Options FollowSymLinks MultiViews + AllowOverride All + Require all granted + + \ No newline at end of file diff --git a/apache/webtrees.conf b/apache/webtrees.conf new file mode 100644 index 0000000..be77403 --- /dev/null +++ b/apache/webtrees.conf @@ -0,0 +1,10 @@ + + ServerName webtrees + DocumentRoot "/var/www/webtrees/" + + + Options FollowSymLinks MultiViews + AllowOverride All + Require all granted + + \ No newline at end of file diff --git a/docker-entrypoint.py b/docker-entrypoint.py new file mode 100644 index 0000000..bfae833 --- /dev/null +++ b/docker-entrypoint.py @@ -0,0 +1,602 @@ +import filecmp +import os +import shutil +import socket +import subprocess +import sys +import time +import urllib.error +from dataclasses import dataclass +from enum import Enum +from typing import Any, List, Literal, Optional, TypeVar, Union, overload +from urllib import request +from urllib.parse import urlencode + + +class NoRedirect(request.HTTPRedirectHandler): + def redirect_request(self, req, fp, code, msg, headers, newurl): + return None + + +class DBType(Enum): + mysql = "mysql" + pgsql = "pgsql" + sqlite = "sqlite" + + +@dataclass +class EnvVars: + prettyurls: bool + https: bool + httpsredirect: bool + sslcertfile: str + sslcertkeyfile: str + lang: str + baseurl: Optional[str] + dbtype: DBType + dbhost: Optional[str] + dbport: str + dbuser: str + dbpass: Optional[str] + dbname: str + tblpfx: str + wtuser: Optional[str] + wtname: Optional[str] + wtpass: Optional[str] + wtemail: Optional[str] + # https://github.com/fisharebest/webtrees/blob/f9a3af650116d75f1a87f454cabff5e9047e43f3/app/Http/Middleware/UseDatabase.php#L71-L82 + dbkey: Optional[str] + dbcert: Optional[str] + dbca: Optional[str] + dbverify: bool + # php settings + phpmemorylimit: str + phpmaxexecutiontime: str + phppostmaxsize: str + phpuploadmaxfilesize: str + # user/group ID + puid: str + pgid: str + + +def truish(value: Optional[str]) -> bool: + """ + Check if a value is close enough to true + """ + if value is None: + return False + + return value.lower().strip() in ["true", "yes", "1"] + + +def print2(msg: Any) -> None: + """ + Print a message to stderr. + """ + print(f"[NV_INIT] {msg}", file=sys.stderr) + + +T = TypeVar("T") + + +@overload +def get_environment_variable( + key: str, default: None = None, alternates: Optional[List[str]] = None +) -> Optional[str]: ... + + +@overload +def get_environment_variable( + key: str, default: T = None, alternates: Optional[List[str]] = None +) -> T: ... + + +def get_environment_variable( + key: str, default: Optional[T] = None, alternates: Optional[List[str]] = None +) -> Union[Optional[str], T]: + """ + Try to find the value of an environment variable. + """ + key = key.upper() + + # try to find variable in env + if key in os.environ: + value = os.environ[key] + + print2(f"{key} found in environment variables") + return value + + # try to find file version of variable + file_key = f"{key}_FILE" + + if file_key in os.environ: + # file name does not exist + if not os.path.isfile(os.environ[file_key]): + print(f"WARNING: {file_key} is not a file: {os.environ[file_key]}") + return None + + # read data from file + with open(os.environ[file_key], "r") as f: + value = f.read().strip() + + print2(f"{file_key} found in environment variables") + return value + + # try to find alternate variable + if alternates is not None: + for a in alternates: + a_value = get_environment_variable(a) + if a_value is not None: + return a_value + + # return default value + print2(f"{key} NOT found in environment variables, using default: {default}") + return default + + +ENV = EnvVars( + prettyurls=truish(get_environment_variable("PRETTY_URLS")), + https=truish(get_environment_variable("HTTPS", alternates=["SSL"])), + httpsredirect=truish( + get_environment_variable("HTTPS_REDIRECT", alternates=["SSL_REDIRECT"]) + ), + sslcertfile=get_environment_variable("SSL_CERT_FILE", "/certs/webtrees.crt"), + sslcertkeyfile=get_environment_variable("SSL_CERT_KEY_FILE", "/certs/webtrees.key"), + baseurl=get_environment_variable("BASE_URL"), + lang=get_environment_variable("LANG", "en-US"), + dbtype=DBType[get_environment_variable("DB_TYPE", "mysql")], + dbhost=get_environment_variable("DB_HOST"), + dbport=get_environment_variable("DB_PORT", "3306"), + dbuser=get_environment_variable( + "DB_USER", + "webtrees", + alternates=["MYSQL_USER", "MARIADB_USER", "POSTGRES_USER"], + ), + dbpass=get_environment_variable( + "DB_PASS", + alternates=["MYSQL_PASSWORD", "MARIADB_PASSWORD", "POSTGRES_PASSWORD"], + ), + dbname=get_environment_variable( + "DB_NAME", + default="webtrees", + alternates=["MYSQL_DATABASE", "MARIADB_DATABASE", "POSTGRES_DB"], + ), + tblpfx=get_environment_variable("DB_PREFIX", "wt_"), + wtuser=get_environment_variable("WT_USER"), + wtname=get_environment_variable("WT_NAME"), + wtpass=get_environment_variable("WT_PASS"), + wtemail=get_environment_variable("WT_EMAIL"), + dbkey=get_environment_variable("DB_KEY"), + dbcert=get_environment_variable("DB_CERT"), + dbca=get_environment_variable("DB_CA"), + dbverify=truish(get_environment_variable("DB_VERIFY")), + phpmemorylimit=get_environment_variable("PHP_MEMORY_LIMIT", "1024M"), + phpmaxexecutiontime=get_environment_variable("PHP_MAX_EXECUTION_TIME", "90"), + phppostmaxsize=get_environment_variable("PHP_POST_MAX_SIZE", "50M"), + phpuploadmaxfilesize=get_environment_variable("PHP_UPLOAD_MAX_FILE_SIZE", "50M"), + puid=get_environment_variable("PUID", "33"), # www-data user + pgid=get_environment_variable("PGID", "33"), +) + + +ROOT = "/var/www/webtrees" +DATA_DIR = os.path.join(ROOT, "data") +CONFIG_FILE = os.path.join(DATA_DIR, "config.ini.php") +PHP_INI_FILE = "/usr/local/etc/php/php.ini" + +os.chdir(ROOT) + + +def retry_urlopen(url: str, data: bytes) -> None: + """ + Retry a request until a postiive repsonse code is reached. Raises error if it fails. + """ + opener = request.build_opener(NoRedirect) + request.install_opener(opener) + + for try_ in range(10): + try: + # make request + print2(f"Attempt {try_} for {url}") + resp = request.urlopen(url, data) + except urllib.error.HTTPError as e: + # capture error as well + resp = e + print2(f"Recieved HTTP {resp.status} response") + + # check status code + # 302 is also accpetable in case the user selected something other than port 80 + if resp.status in (200, 302): + return + + # backoff + time.sleep(try_) + + raise RuntimeError(f"Could not send a request to {url}") + + +def add_line_to_file(filename: str, newline: str) -> None: + """ + Add a new line to a file. If an existing line is found with the same + starting string, it will be replaced. + """ + newline += "\n" + + # read file + with open(filename, "r") as fp: + lines = fp.readlines() + + key = newline.split("=")[0] + + # replace matching line + found = False + + for i, line in enumerate(lines): + if line.startswith(key): + if line == newline: + return + + lines[i] = newline + found = True + break + + if not found: + lines.append(newline) + + # write new contents + with open(filename, "w") as fp: + fp.writelines(lines) + + +def set_config_value(key: str, value: Optional[str]) -> None: + """ + In the config file, make sure the given key is set to the given value. + """ + if value is None: + return + + print2(f"Setting value for {key} in config") + + if not os.path.isfile(CONFIG_FILE): + print2(f"WARNING: {CONFIG_FILE} does not exist") + return + + add_line_to_file(CONFIG_FILE, f'{key}="{value}"') + + +def set_php_ini_value(key: str, value: str) -> None: + """ + In the php.ini file, make sure the given key is set to the given value. + """ + print2(f"Setting value for {key} in php.ini") + add_line_to_file(PHP_INI_FILE, f"{key} = {value}") + + +def enable_apache_site( + enable_sites: List[Literal["webtrees", "webtrees-redir", "webtrees-ssl"]], +) -> None: + """ + Enable an Apache site. + """ + + # update ssl apache config with cert path from env + ssl_site_file = "/etc/apache2/sites-available/webtrees-ssl.conf" + + # make paths absolute + if not os.path.isabs(ENV.sslcertfile): + ENV.sslcertfile = os.path.join(DATA_DIR, ENV.sslcertfile) + + if not os.path.isabs(ENV.sslcertkeyfile): + ENV.sslcertkeyfile = os.path.join(DATA_DIR, ENV.sslcertkeyfile) + + # update file + with open(ssl_site_file, "r") as fp: + ssl_site_file_lines = fp.readlines() + + new_ssl_site_file_lines = [] + for line in ssl_site_file_lines: + if line.strip().startswith("SSLCertificateFile"): + line = line.replace(line.split()[1], ENV.sslcertfile) + elif line.strip().startswith("SSLCertificateKeyFile"): + line = line.replace(line.split()[1], ENV.sslcertkeyfile) + + new_ssl_site_file_lines.append(line) + + with open(ssl_site_file, "w") as fp: + fp.writelines(new_ssl_site_file_lines) + + all_sites = ["webtrees", "webtrees-redir", "webtrees-ssl"] + + # perl complains about locale to stderr, so disable that + + # disable the other sites + for s in all_sites: + if s not in enable_sites: + print2(f"Disabling site {s}") + subprocess.check_call( + ["a2dissite", s], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL + ) + + # enable the desired sites + for s in enable_sites: + print2(f"Enabling site {s}") + subprocess.check_call( + ["a2ensite", s], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL + ) + + +def perms() -> None: + """ + Set up folder permissions + """ + + print2("Setting up folder permissions for uploads") + # https://github.com/linuxserver/docker-baseimage-alpine/blob/bef0f4cee208396c92c0fdd1426613de02698301/root/etc/s6-overlay/s6-rc.d/init-adduser/run#L4-L9 + subprocess.check_call(["groupmod", "-o", "-g", ENV.pgid, "www-data"]) + subprocess.check_call(["usermod", "-o", "-u", ENV.puid, "www-data"]) + subprocess.check_call(["chown", "-R", "www-data:www-data", DATA_DIR]) + + if os.path.isfile(CONFIG_FILE): + subprocess.check_call(["chmod", "700", CONFIG_FILE]) + + +def php_ini() -> None: + """ + Update PHP .ini file + """ + print2("Updating php.ini") + + if not os.path.isfile(PHP_INI_FILE): + print2("Creating php.ini") + + os.makedirs(os.path.dirname(PHP_INI_FILE), exist_ok=True) + with open(PHP_INI_FILE, "w") as fp: + fp.writelines(["[PHP]\n", "\n"]) + + set_php_ini_value("memory_limit", ENV.phpmemorylimit) + set_php_ini_value("max_execution_time", ENV.phpmaxexecutiontime) + set_php_ini_value("post_max_size", ENV.phppostmaxsize) + set_php_ini_value("upload_max_filesize", ENV.phpuploadmaxfilesize) + + # https://webtrees.net/admin/performance/ + set_php_ini_value("opcache.enable", "1") + set_php_ini_value( + "opcache.revalidate_freq", "60" + ) # re check changed files every 60 seconds + set_php_ini_value("opcache.revalidate_path", "0") + + +def check_db_variables() -> bool: + """ + Check if all required database variables are present + """ + try: + assert ENV.dbtype is not None + assert ENV.dbname is not None + assert ENV.tblpfx is not None + + if ENV.dbtype == DBType.sqlite: + ENV.dbhost = "" + ENV.dbport = "" + ENV.dbuser = "" + ENV.dbpass = "" + + assert ENV.dbhost is not None + assert ENV.dbport is not None + assert ENV.dbuser is not None + assert ENV.dbpass is not None + + except AssertionError: + print2("WARNING: Not all database variables are set") + return False + + return True + + +def setup_wizard() -> None: + """ + Run the setup wizard + """ + + if os.path.isfile(CONFIG_FILE): + return + + print2("Attempting to automate setup wizard") + + # make sure all the variables we need are not set to None + if not check_db_variables(): + return + + if any( + v is None + for v in [ENV.baseurl, ENV.wtname, ENV.wtuser, ENV.wtpass, ENV.wtemail] + ): + print2("WARNING: Not all required variables were found for setup wizard") + return + + assert ENV.baseurl is not None + if not ENV.baseurl.startswith("http"): + print2( + "WARNING: BASE_URL does not start with 'http'. This is likely not what you want." + ) + + print2("Automating setup wizard") + print2("Starting Apache in background") + # set us up to a known HTTP state + enable_apache_site(["webtrees"]) + # run apache in the background + apache_proc = subprocess.Popen(["apache2-foreground"], stderr=subprocess.DEVNULL) + + if ENV.dbtype in [DBType.mysql, DBType.pgsql]: + # for typing, check_db_variables already does this + assert ENV.dbhost is not None + + # try to resolve the host + # most common error is wrong hostname + try: + socket.gethostbyname(ENV.dbhost) + except socket.gaierror: + print2(f"ERROR: Could not resolve database host '{ENV.dbhost}'") + print2( + "ERROR: You likely have the DBHOST environment variable set incorrectly." + ) + print2("ERROR: Exiting.") + + # stop apache + apache_proc.terminate() + # die + sys.exit(1) + + # wait until database is ready + if ENV.dbtype == DBType.mysql: + # https://dev.mysql.com/doc/refman/8.0/en/mysqladmin.html#option_mysqladmin_user + # don't miss the capital P + cmd = ["mysqladmin", "ping", "-h", ENV.dbhost, "-P", ENV.dbport, "--silent"] + name = "MySQL" + elif ENV.dbtype == DBType.pgsql: + # https://www.postgresql.org/docs/current/app-pg-isready.html + cmd = ["pg_isready", "-h", ENV.dbhost, "-p", ENV.dbport, "--quiet"] + name = "PostgreSQL" + + while subprocess.run(cmd).returncode != 0: # type: ignore + print2(f"Waiting for {name} server {ENV.dbhost}:{ENV.dbport} to be ready") # type: ignore + time.sleep(1) + + else: + # sqlite + # let Apache start up + time.sleep(2) + + # send it + url = "http://127.0.0.1:80/" + print2(f"Sending setup wizard request to {url}") + + retry_urlopen( + url, + urlencode( + { + "lang": ENV.lang, + "tblpfx": ENV.tblpfx, + "baseurl": ENV.baseurl, + "dbtype": ENV.dbtype.value, + "dbhost": ENV.dbhost, + "dbport": ENV.dbport, + "dbuser": ENV.dbuser, + "dbpass": ENV.dbpass, + "dbname": ENV.dbname, + "wtname": ENV.wtname, + "wtuser": ENV.wtuser, + "wtpass": ENV.wtpass, + "wtemail": ENV.wtemail, + "step": "6", + } + ).encode("ascii"), + ) + + print2("Stopping Apache") + apache_proc.terminate() + + +def update_config_file() -> None: + """ + Update the config file with items set via environment variables + """ + print2("Updating config file") + + if not os.path.isfile(CONFIG_FILE): + print2(f"Config file not found at {CONFIG_FILE}. Nothing to update.") + return + + # update independent values + set_config_value("rewrite_urls", str(int(ENV.prettyurls))) + set_config_value("base_url", ENV.baseurl) + + # update database values as a group + if check_db_variables(): + set_config_value("dbtype", ENV.dbtype.value) + set_config_value("dbhost", ENV.dbhost) + set_config_value("dbport", ENV.dbport) + set_config_value("dbuser", ENV.dbuser) + set_config_value("dbpass", ENV.dbpass) + set_config_value("dbname", ENV.dbname) + set_config_value("tblpfx", ENV.tblpfx) + + # update databases verification values + if ENV.dbtype == DBType.mysql and all( + v is not None for v in [ENV.dbkey, ENV.dbcert, ENV.dbca] + ): + set_config_value("dbkey", ENV.dbkey) + set_config_value("dbcert", ENV.dbcert) + set_config_value("dbca", ENV.dbca) + set_config_value("dbverify", str(int(ENV.dbverify))) + + +def https() -> None: + """ + Configure enabled Apache sites + """ + print2("Configuring HTTPS") + + # no https + if not ENV.https: + print2("Removing HTTPS") + enable_apache_site(["webtrees"]) + # https with redirect + elif ENV.httpsredirect: + print2("Adding HTTPS, with HTTPS redirect") + enable_apache_site(["webtrees-ssl", "webtrees-redir"]) + # https no redirect + else: + print2("Adding HTTPS, removing HTTPS redirect") + enable_apache_site(["webtrees", "webtrees-ssl"]) + + +def htaccess() -> None: + """ + Recreate .htaccess file if it ever deletes itself in the /data/ directory + """ + src_htaccess_file = os.path.join("/bak", "webtrees", "data", ".htaccess") + htaccess_file = os.path.join(DATA_DIR, ".htaccess") + + if not os.path.isfile(src_htaccess_file): + # skip if the backup file does not exist + print2( + f"WARNING: {src_htaccess_file} does not exist. Skipping {htaccess_file} integrity check" + ) + return + + # https://github.com/NathanVaughn/webtrees-docker/issues/188 + # In the event the htaccess file in the data directory is different + # from what it's supposed to be, replace it. + + if os.path.isfile(htaccess_file) and filecmp.cmp(src_htaccess_file, htaccess_file): + print2(f"{htaccess_file} integrity check succeeded") + return + + print2(f"WARNING: {htaccess_file} does not exist or does not match master copy") + shutil.copyfile(src_htaccess_file, htaccess_file) + print2(f"Updated {htaccess_file}") + + +def main() -> None: + # first, set up permissions + perms() + # create php config + php_ini() + # run the setup wizard if the config file doesn't exist + setup_wizard() + # update the config file + update_config_file() + # configure https + https() + # make sure .htaccess exists + htaccess() + # set up permissions again + perms() + + print2("Starting Apache") + subprocess.run(["apache2-foreground"], stderr=subprocess.DEVNULL) + + +if __name__ == "__main__": + main() diff --git a/docker-healthcheck.sh b/docker-healthcheck.sh new file mode 100644 index 0000000..de60661 --- /dev/null +++ b/docker-healthcheck.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +# https://github.com/fisharebest/webtrees/blob/7f1eef18821463166c14403c3d0e0aa3ba7a4204/app/Http/Middleware/BadBotBlocker.php#L335 +# https://github.com/fisharebest/webtrees/blob/7f1eef18821463166c14403c3d0e0aa3ba7a4204/app/Http/Middleware/BadBotBlocker.php#L313-L318 +# https://github.com/NathanVaughn/webtrees-docker/issues/164 + +if ls /etc/apache2/sites-enabled | grep -q 'ssl'; +then + curl --verbose --silent --insecure --fail --cookie-jar /tmp/cookie.txt --cookie "x=y" --cookie /tmp/cookie.txt --user-agent "Chrome/" https://127.0.0.1:443/ || exit 1 +else + curl --verbose --silent --fail --cookie-jar /tmp/cookie.txt --cookie "x=y" --cookie /tmp/cookie.txt --user-agent "Chrome/" http://127.0.0.1:80/ || exit 1 +fi diff --git a/patches/UpgradeService1.patch b/patches/UpgradeService1.patch new file mode 100644 index 0000000..82856ed --- /dev/null +++ b/patches/UpgradeService1.patch @@ -0,0 +1,39 @@ +diff --git "a/.\\UpgradeService.php" "b/.\\UpgradeServicePatched.php" +index b69bea0..c3e682c 100644 +--- "a/.\\UpgradeService.php" ++++ "b/.\\UpgradeServicePatched.php" +@@ -339,34 +339,6 @@ class UpgradeService + */ + private function fetchLatestVersion(bool $force): string + { +- $last_update_timestamp = (int) Site::getPreference('LATEST_WT_VERSION_TIMESTAMP'); +- +- $current_timestamp = time(); +- +- if ($force || $last_update_timestamp < $current_timestamp - self::CHECK_FOR_UPDATE_INTERVAL) { +- try { +- $client = new Client([ +- 'timeout' => self::HTTP_TIMEOUT, +- ]); +- +- $response = $client->get(self::UPDATE_URL, [ +- 'query' => $this->serverParameters(), +- ]); +- +- if ($response->getStatusCode() === StatusCodeInterface::STATUS_OK) { +- Site::setPreference('LATEST_WT_VERSION', $response->getBody()->getContents()); +- Site::setPreference('LATEST_WT_VERSION_TIMESTAMP', (string) $current_timestamp); +- Site::setPreference('LATEST_WT_VERSION_ERROR', ''); +- } else { +- Site::setPreference('LATEST_WT_VERSION_ERROR', 'HTTP' . $response->getStatusCode()); +- } +- } catch (GuzzleException $ex) { +- // Can't connect to the server? +- // Use the existing information about latest versions. +- Site::setPreference('LATEST_WT_VERSION_ERROR', $ex->getMessage()); +- } +- } +- + return Site::getPreference('LATEST_WT_VERSION'); + } + diff --git a/patches/UpgradeService2.patch b/patches/UpgradeService2.patch new file mode 100644 index 0000000..950a65a --- /dev/null +++ b/patches/UpgradeService2.patch @@ -0,0 +1,39 @@ +diff --git "a/.\\UpgradeService.php" "b/.\\UpgradeServicePatched.php" +index 6bb3504..a630dfb 100644 +--- "a/.\\UpgradeService.php" ++++ "b/.\\UpgradeServicePatched.php" +@@ -337,34 +337,6 @@ class UpgradeService + */ + private function fetchLatestVersion(bool $force): string + { +- $last_update_timestamp = (int) Site::getPreference('LATEST_WT_VERSION_TIMESTAMP'); +- +- $current_timestamp = time(); +- +- if ($force || $last_update_timestamp < $current_timestamp - self::CHECK_FOR_UPDATE_INTERVAL) { +- Site::setPreference('LATEST_WT_VERSION_TIMESTAMP', (string) $current_timestamp); +- +- try { +- $client = new Client([ +- 'timeout' => self::HTTP_TIMEOUT, +- ]); +- +- $response = $client->get(self::UPDATE_URL, [ +- 'query' => $this->serverParameters(), +- ]); +- +- if ($response->getStatusCode() === StatusCodeInterface::STATUS_OK) { +- Site::setPreference('LATEST_WT_VERSION', $response->getBody()->getContents()); +- Site::setPreference('LATEST_WT_VERSION_ERROR', ''); +- } else { +- Site::setPreference('LATEST_WT_VERSION_ERROR', 'HTTP' . $response->getStatusCode()); +- } +- } catch (GuzzleException $ex) { +- // Can't connect to the server? +- // Use the existing information about latest versions. +- Site::setPreference('LATEST_WT_VERSION_ERROR', $ex->getMessage()); +- } +- } + + return Site::getPreference('LATEST_WT_VERSION'); + }