feat: webtrees docker build setup with patched PHP 8.4.26 (CVE-2026-109618 / CVE-2026-109619)
This commit is contained in:
@@ -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]
|
||||
+78
@@ -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"]
|
||||
@@ -4,10 +4,10 @@
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<h1 align="center">Name des Projekts</h1>
|
||||
<h1 align="center">Webtrees Docker (PHP 8.4 Security Patched)</h1>
|
||||
|
||||
<h4 align="center">
|
||||
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")
|
||||
</h4>
|
||||
|
||||
<h6 align="center">
|
||||
@@ -21,9 +21,115 @@
|
||||
</h6>
|
||||
<br><br>
|
||||
|
||||
## 📌 Ü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/))
|
||||
|
||||
<br><br>
|
||||
<p align="center">
|
||||
@@ -32,6 +138,6 @@ CONTENT BEREICH
|
||||
|
||||
<p align="center">
|
||||
<sub>
|
||||
© Patrick Asmus · Techniverse Network · <a href="./LICENSE">Lizenz</a>
|
||||
© Patrick Asmus · Techniverse Network · <a href="./LICENSE.gnu">GPL-3.0 Lizenz</a>
|
||||
</sub>
|
||||
</p>
|
||||
</p>
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
<VirtualHost *:80>
|
||||
ServerName webtrees
|
||||
DocumentRoot "/var/www/webtrees/"
|
||||
|
||||
RewriteEngine On
|
||||
RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [END,NE,R=permanent]
|
||||
|
||||
<Directory "/var/www/webtrees/">
|
||||
Options FollowSymLinks MultiViews
|
||||
AllowOverride All
|
||||
Require all granted
|
||||
</Directory>
|
||||
</VirtualHost>
|
||||
@@ -0,0 +1,14 @@
|
||||
<VirtualHost *:443>
|
||||
ServerName webtrees
|
||||
DocumentRoot "/var/www/webtrees/"
|
||||
|
||||
SSLEngine on
|
||||
SSLCertificateFile /certs/webtrees.crt
|
||||
SSLCertificateKeyFile /certs/webtrees.key
|
||||
|
||||
<Directory "/var/www/webtrees/">
|
||||
Options FollowSymLinks MultiViews
|
||||
AllowOverride All
|
||||
Require all granted
|
||||
</Directory>
|
||||
</VirtualHost>
|
||||
@@ -0,0 +1,10 @@
|
||||
<VirtualHost *:80>
|
||||
ServerName webtrees
|
||||
DocumentRoot "/var/www/webtrees/"
|
||||
|
||||
<Directory "/var/www/webtrees/">
|
||||
Options FollowSymLinks MultiViews
|
||||
AllowOverride All
|
||||
Require all granted
|
||||
</Directory>
|
||||
</VirtualHost>
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
Reference in New Issue
Block a user