mirror of
https://github.com/eduardogsilva/wireguard_webadmin.git
synced 2026-03-15 13:36:18 +00:00
add script to generate wireguard_webadmin.json
This commit is contained in:
@@ -40,3 +40,4 @@ venv.bak/
|
|||||||
/.vscode
|
/.vscode
|
||||||
.tmp/
|
.tmp/
|
||||||
wireguard_webadmin/production_settings.py
|
wireguard_webadmin/production_settings.py
|
||||||
|
containers/caddy/wireguard_webadmin.json
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -40,3 +40,4 @@ venv.bak/
|
|||||||
/.vscode
|
/.vscode
|
||||||
.tmp/
|
.tmp/
|
||||||
wireguard_webadmin/production_settings.py
|
wireguard_webadmin/production_settings.py
|
||||||
|
containers/caddy/wireguard_webadmin.json
|
||||||
@@ -2,223 +2,9 @@
|
|||||||
|
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
MANUAL_CERT_DIR="${MANUAL_CERT_DIR:-/certificates}"
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
CADDYFILE_PATH="${CADDYFILE_PATH:-/etc/caddy/Caddyfile}"
|
PYTHON="${SCRIPT_DIR}/.venv/bin/python3"
|
||||||
|
|
||||||
trim_value() {
|
"$PYTHON" "${SCRIPT_DIR}/export_wireguard_webadmin_config.py"
|
||||||
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
|
|
||||||
|
|
||||||
exec "$@"
|
exec "$@"
|
||||||
|
|||||||
137
containers/caddy/export_wireguard_webadmin_config.py
Normal file
137
containers/caddy/export_wireguard_webadmin_config.py
Normal 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()
|
||||||
Reference in New Issue
Block a user