From 2030ec5904fca5dfb0358b1a96e94d6fbc9601b6 Mon Sep 17 00:00:00 2001 From: Eduardo Silva Date: Sat, 14 Mar 2026 10:41:39 -0300 Subject: [PATCH] add script to generate wireguard_webadmin.json --- .dockerignore | 3 +- .gitignore | 3 +- containers/caddy/entrypoint.sh | 220 +----------------- .../caddy/export_wireguard_webadmin_config.py | 137 +++++++++++ 4 files changed, 144 insertions(+), 219 deletions(-) create mode 100644 containers/caddy/export_wireguard_webadmin_config.py diff --git a/.dockerignore b/.dockerignore index 2191ab0..5f1ace9 100644 --- a/.dockerignore +++ b/.dockerignore @@ -39,4 +39,5 @@ venv.bak/ .vscode/ /.vscode .tmp/ -wireguard_webadmin/production_settings.py \ No newline at end of file +wireguard_webadmin/production_settings.py +containers/caddy/wireguard_webadmin.json \ No newline at end of file diff --git a/.gitignore b/.gitignore index 2191ab0..5f1ace9 100644 --- a/.gitignore +++ b/.gitignore @@ -39,4 +39,5 @@ venv.bak/ .vscode/ /.vscode .tmp/ -wireguard_webadmin/production_settings.py \ No newline at end of file +wireguard_webadmin/production_settings.py +containers/caddy/wireguard_webadmin.json \ No newline at end of file diff --git a/containers/caddy/entrypoint.sh b/containers/caddy/entrypoint.sh index 42c1f68..0ba06ab 100644 --- a/containers/caddy/entrypoint.sh +++ b/containers/caddy/entrypoint.sh @@ -2,223 +2,9 @@ set -eu -MANUAL_CERT_DIR="${MANUAL_CERT_DIR:-/certificates}" -CADDYFILE_PATH="${CADDYFILE_PATH:-/etc/caddy/Caddyfile}" +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PYTHON="${SCRIPT_DIR}/.venv/bin/python3" -trim_value() { - printf '%s' "$1" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' -} - -is_valid_hostname() { - host_value="$1" - - if [ -z "$host_value" ]; then - return 1 - fi - - if is_ip_host "$host_value"; then - return 1 - fi - - if printf '%s' "$host_value" | grep -Eq '^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)*$'; then - return 0 - fi - - return 1 -} - -is_ip_host() { - host_value="$1" - - case "$host_value" in - \[*\]|*:*:*) - return 0 - ;; - *) - ;; - esac - - if printf '%s' "$host_value" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$'; then - return 0 - fi - - return 1 -} - -is_internal_tls_host() { - host_value="$1" - - case "$host_value" in - localhost|*.localhost) - return 0 - ;; - esac - - if is_ip_host "$host_value"; then - return 0 - fi - - return 1 -} - -normalize_host() { - raw_value="$(trim_value "$1")" - raw_value="${raw_value#http://}" - raw_value="${raw_value#https://}" - raw_value="${raw_value%%/*}" - - case "$raw_value" in - \[*\]:*) - printf '%s' "${raw_value%:*}" - ;; - *:*) - case "$raw_value" in - *:*:*) - printf '%s' "$raw_value" - ;; - *) - printf '%s' "${raw_value%%:*}" - ;; - esac - ;; - *) - printf '%s' "$raw_value" - ;; - esac -} - -append_host() { - candidate_host="$(normalize_host "$1")" - - if [ -z "$candidate_host" ]; then - return - fi - - case " -$HOSTS -" in - *" -$candidate_host -"*) - return - ;; - esac - - if [ -z "$HOSTS" ]; then - HOSTS="$candidate_host" - return - fi - - HOSTS="$HOSTS -$candidate_host" -} - -append_dns_host() { - candidate_host="$(normalize_host "$1")" - - if [ -z "$candidate_host" ]; then - return - fi - - if is_internal_tls_host "$candidate_host"; then - return - fi - - append_host "$candidate_host" -} - -build_common_block() { - cat <<'EOF' - import wireguard_common -EOF -} - -build_tls_block() { - domain_name="$1" - cert_file="${MANUAL_CERT_DIR}/${domain_name}/fullchain.pem" - key_file="${MANUAL_CERT_DIR}/${domain_name}/key.pem" - - if [ -f "$cert_file" ] && [ -f "$key_file" ]; then - cat < "$CADDYFILE_PATH" <<'EOF' -(wireguard_common) { - encode gzip - - @static path /static/* - handle @static { - root * /static - uri strip_prefix /static - file_server - header Cache-Control "public, max-age=3600" - } - - handle { - reverse_proxy wireguard-webadmin:8000 { - header_up Host {host} - } - } -} -EOF - -printf '%s\n' "$HOSTS" | while IFS= read -r current_host; do - if [ -z "$current_host" ]; then - continue - fi - - { - printf '\n%s {\n' "$current_host" - build_common_block - build_tls_block "$current_host" - printf '}\n' - } >> "$CADDYFILE_PATH" -done - -if command -v caddy >/dev/null 2>&1; then - caddy fmt --overwrite "$CADDYFILE_PATH" >/dev/null 2>&1 -fi +"$PYTHON" "${SCRIPT_DIR}/export_wireguard_webadmin_config.py" exec "$@" diff --git a/containers/caddy/export_wireguard_webadmin_config.py b/containers/caddy/export_wireguard_webadmin_config.py new file mode 100644 index 0000000..a43163c --- /dev/null +++ b/containers/caddy/export_wireguard_webadmin_config.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python3 +""" +Generates wireguard_webadmin.json from environment variables. + +The output format matches the schema defined in config_example/wireguard_webadmin.json +and is intended to be processed by a separate configuration pipeline. +""" + +import json +import os +import re +import sys + +OUTPUT_FILE = os.path.join(os.path.dirname(__file__), "wireguard_webadmin.json") + +UPSTREAM = "wireguard-webadmin:8000" +STATIC_ROUTES = [ + { + "path_prefix": "/static", + "root": "/static", + "strip_prefix": "/static", + "cache_control": "public, max-age=3600", + } +] + +IPV4_RE = re.compile(r"^\d+\.\d+\.\d+\.\d+$") +HOSTNAME_RE = re.compile( + r"^[A-Za-z0-9]([A-Za-z0-9\-]*[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9\-]*[A-Za-z0-9])?)*$" +) + + +def normalize_host(raw: str) -> str: + """Strip scheme, path, and port from a host string.""" + raw = raw.strip() + for scheme in ("https://", "http://"): + if raw.startswith(scheme): + raw = raw[len(scheme):] + raw = raw.split("/")[0] + + # IPv6 bracketed address with port: [::1]:8080 → [::1] + if raw.startswith("[") and "]:" in raw: + raw = raw.rsplit(":", 1)[0] + return raw + + # Plain IPv4 or hostname with port: host:8080 → host (not IPv6) + if raw.count(":") == 1: + raw = raw.split(":")[0] + + return raw + + +def is_ip(host: str) -> bool: + """Return True if host is an IPv4 address or an IPv6 address.""" + if IPV4_RE.match(host): + return True + if host.startswith("[") or ":" in host: + return True + return False + + +def is_valid_hostname(host: str) -> bool: + """Return True only for proper DNS hostnames (no IPs).""" + if not host: + return False + if is_ip(host): + return False + return bool(HOSTNAME_RE.match(host)) + + +def collect_hosts() -> list: + """Build the ordered, deduplicated list of hosts from environment variables.""" + seen = set() + hosts = [] + + def add_host(host: str) -> None: + normalized = normalize_host(host) + if normalized and normalized not in seen: + seen.add(normalized) + hosts.append(normalized) + + primary = os.environ.get("SERVER_ADDRESS", "").strip() + primary_normalized = normalize_host(primary) + + if not is_valid_hostname(primary_normalized): + print( + f"Error: SERVER_ADDRESS must be a valid hostname, not an IP address. " + f"Received: {primary!r}", + file=sys.stderr, + ) + sys.exit(1) + + add_host(primary) + + for extra in os.environ.get("EXTRA_ALLOWED_HOSTS", "").split(","): + extra = extra.strip() + if not extra: + continue + normalized = normalize_host(extra) + # Skip IPs and localhost-style entries (handled by TLS internally) + if is_ip(normalized) or normalized in ("localhost",) or normalized.endswith(".localhost"): + continue + add_host(extra) + + if not hosts: + print("Error: no valid hostnames were collected.", file=sys.stderr) + sys.exit(1) + + return hosts + + +def build_config(hosts: list) -> dict: + return { + "entries": [ + { + "id": "wireguard_webadmin", + "name": "WireGuard WebAdmin", + "hosts": hosts, + "upstream": UPSTREAM, + "static_routes": STATIC_ROUTES, + } + ] + } + + +def main() -> None: + hosts = collect_hosts() + config = build_config(hosts) + + with open(OUTPUT_FILE, "w", encoding="utf-8") as output_file: + json.dump(config, output_file, indent=2) + output_file.write("\n") + + print(f"Config written to {OUTPUT_FILE}") + + +if __name__ == "__main__": + main()