add script to generate wireguard_webadmin.json

This commit is contained in:
Eduardo Silva
2026-03-14 10:41:39 -03:00
parent 1097ae90cf
commit 2030ec5904
4 changed files with 144 additions and 219 deletions

View File

@@ -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 <<EOF
tls ${cert_file} ${key_file}
EOF
return
fi
if is_internal_tls_host "$domain_name"; then
cat <<'EOF'
tls internal
EOF
return
fi
cat <<'EOF'
tls {
issuer acme
issuer internal
}
EOF
}
HOSTS=""
PRIMARY_HOST="$(normalize_host "${SERVER_ADDRESS:-}")"
if ! is_valid_hostname "$PRIMARY_HOST"; then
echo "SERVER_ADDRESS must be a hostname, not an IP address. Received: ${SERVER_ADDRESS:-}"
exit 1
fi
append_host "$PRIMARY_HOST"
ORIGINAL_IFS="$IFS"
IFS=','
for configured_host in ${EXTRA_ALLOWED_HOSTS:-}; do
append_dns_host "$configured_host"
done
IFS="$ORIGINAL_IFS"
if [ -z "$HOSTS" ]; then
echo "No valid hostnames were provided for Caddy."
exit 1
fi
mkdir -p "$(dirname "$CADDYFILE_PATH")"
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 "$@"

View File

@@ -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()