2026-03-14 22:56:47 -03:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
"""
|
2026-03-16 09:49:17 -03:00
|
|
|
Reads JSON config files exported by Django and generates /etc/caddy/Caddyfile.
|
2026-03-14 22:56:47 -03:00
|
|
|
|
|
|
|
|
Expected input files in /caddy_json_export/:
|
|
|
|
|
- wireguard_webadmin.json (required, generated on container startup)
|
|
|
|
|
- applications.json (optional, exported from Django)
|
|
|
|
|
- auth_policies.json (optional, exported from Django)
|
|
|
|
|
- routes.json (optional, exported from Django)
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import json
|
|
|
|
|
import os
|
2026-03-16 09:49:17 -03:00
|
|
|
from urllib.parse import urlparse
|
2026-03-14 22:56:47 -03:00
|
|
|
|
|
|
|
|
JSON_DIR = os.environ.get("JSON_DIR", "/caddy_json_export")
|
|
|
|
|
CADDYFILE_PATH = os.environ.get("CADDYFILE_PATH", "/etc/caddy/Caddyfile")
|
2026-03-16 10:34:10 -03:00
|
|
|
AUTH_GATEWAY_INTERNAL_URL = os.environ.get("AUTH_GATEWAY_INTERNAL_URL", "http://wireguard-webadmin-auth-gateway:9091")
|
|
|
|
|
AUTH_GATEWAY_PORTAL_PATH = os.environ.get("AUTH_GATEWAY_EXTERNAL_PATH", "/auth-gateway")
|
2026-03-16 09:49:17 -03:00
|
|
|
AUTH_GATEWAY_CHECK_URI = "/auth/check"
|
2026-03-14 22:56:47 -03:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def load_json(filename):
|
|
|
|
|
filepath = os.path.join(JSON_DIR, filename)
|
|
|
|
|
if not os.path.exists(filepath):
|
|
|
|
|
return None
|
|
|
|
|
with open(filepath, "r", encoding="utf-8") as json_file:
|
|
|
|
|
return json.load(json_file)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def collect_all_applications():
|
|
|
|
|
apps = []
|
|
|
|
|
|
|
|
|
|
webadmin_data = load_json("wireguard_webadmin.json")
|
|
|
|
|
if webadmin_data:
|
|
|
|
|
apps.extend(webadmin_data.get("entries", []))
|
|
|
|
|
|
|
|
|
|
applications_data = load_json("applications.json")
|
|
|
|
|
if applications_data:
|
|
|
|
|
apps.extend(applications_data.get("entries", []))
|
|
|
|
|
|
|
|
|
|
return apps
|
|
|
|
|
|
|
|
|
|
|
2026-03-16 09:49:17 -03:00
|
|
|
def split_upstream(upstream):
|
|
|
|
|
"""Return (base_url, upstream_path) where base_url has no path component.
|
|
|
|
|
Upstreams without an explicit http(s):// scheme are returned as-is with no path."""
|
|
|
|
|
if not upstream.startswith("http://") and not upstream.startswith("https://"):
|
|
|
|
|
return upstream, ""
|
|
|
|
|
parsed = urlparse(upstream)
|
|
|
|
|
base = f"{parsed.scheme}://{parsed.netloc}"
|
|
|
|
|
path = parsed.path.rstrip("/")
|
|
|
|
|
return base, path
|
|
|
|
|
|
|
|
|
|
|
2026-03-14 22:56:47 -03:00
|
|
|
def build_caddyfile(apps, auth_policies, routes):
|
2026-03-16 09:49:17 -03:00
|
|
|
policies = auth_policies.get("policies", {}) if auth_policies else {}
|
|
|
|
|
route_entries = routes.get("entries", {}) if routes else {}
|
2026-03-14 22:56:47 -03:00
|
|
|
lines = []
|
2026-03-15 11:37:25 -03:00
|
|
|
|
|
|
|
|
def get_policy_type(policy_name):
|
|
|
|
|
if policy_name and policy_name in policies:
|
|
|
|
|
return policies[policy_name].get("policy_type", "bypass")
|
2026-03-16 09:49:17 -03:00
|
|
|
return "deny"
|
2026-03-14 22:56:47 -03:00
|
|
|
|
2026-03-16 09:49:17 -03:00
|
|
|
def emit_auth_portal():
|
|
|
|
|
lines.append(f" handle_path {AUTH_GATEWAY_PORTAL_PATH}/* {{")
|
|
|
|
|
lines.append(f" reverse_proxy {AUTH_GATEWAY_INTERNAL_URL}")
|
|
|
|
|
lines.append(" }")
|
|
|
|
|
lines.append("")
|
|
|
|
|
|
|
|
|
|
def handle_open(matcher):
|
|
|
|
|
if matcher == "*":
|
|
|
|
|
return " handle {"
|
|
|
|
|
return f" handle {matcher} {{"
|
|
|
|
|
|
|
|
|
|
def emit_reverse_proxy(base, upstream_path, indent=" "):
|
|
|
|
|
if upstream_path:
|
|
|
|
|
lines.append(f"{indent}rewrite * {upstream_path}{{uri}}")
|
|
|
|
|
lines.append(f"{indent}reverse_proxy {base}")
|
|
|
|
|
|
|
|
|
|
def emit_protected_handle(path_matcher, base, upstream_path):
|
|
|
|
|
lines.append(handle_open(path_matcher))
|
|
|
|
|
lines.append(f" forward_auth {AUTH_GATEWAY_INTERNAL_URL} {{")
|
|
|
|
|
lines.append(f" uri {AUTH_GATEWAY_CHECK_URI}")
|
|
|
|
|
lines.append(" copy_headers X-Auth-User X-Auth-Email X-Auth-Groups X-Auth-Factors X-Auth-Policy")
|
|
|
|
|
lines.append(" }")
|
|
|
|
|
emit_reverse_proxy(base, upstream_path)
|
|
|
|
|
lines.append(" }")
|
2026-03-15 16:30:28 -03:00
|
|
|
lines.append("")
|
|
|
|
|
|
2026-03-14 22:56:47 -03:00
|
|
|
for app in apps:
|
|
|
|
|
hosts = app.get("hosts", [])
|
|
|
|
|
upstream = app.get("upstream", "")
|
|
|
|
|
static_routes = app.get("static_routes", [])
|
2026-03-16 09:49:17 -03:00
|
|
|
app_id = app.get("id", "")
|
2026-03-14 22:56:47 -03:00
|
|
|
|
|
|
|
|
if not hosts or not upstream:
|
|
|
|
|
continue
|
|
|
|
|
|
2026-03-16 09:49:17 -03:00
|
|
|
base, upstream_path = split_upstream(upstream)
|
2026-03-14 22:56:47 -03:00
|
|
|
|
2026-03-16 09:49:17 -03:00
|
|
|
lines.append(f"{', '.join(hosts)} {{")
|
|
|
|
|
emit_auth_portal()
|
2026-03-15 10:16:26 -03:00
|
|
|
|
2026-03-14 22:56:47 -03:00
|
|
|
for static_route in static_routes:
|
|
|
|
|
path_prefix = static_route.get("path_prefix", "")
|
|
|
|
|
root_dir = static_route.get("root", "")
|
|
|
|
|
cache_control = static_route.get("cache_control", "")
|
|
|
|
|
lines.append(f" handle_path {path_prefix}/* {{")
|
|
|
|
|
lines.append(f" root * {root_dir}")
|
2026-03-16 09:49:17 -03:00
|
|
|
lines.append(" file_server")
|
2026-03-14 22:56:47 -03:00
|
|
|
if cache_control:
|
|
|
|
|
lines.append(f" header Cache-Control \"{cache_control}\"")
|
2026-03-16 09:49:17 -03:00
|
|
|
lines.append(" }")
|
2026-03-15 11:37:25 -03:00
|
|
|
lines.append("")
|
|
|
|
|
|
2026-03-16 09:49:17 -03:00
|
|
|
app_route_data = route_entries.get(app_id)
|
|
|
|
|
if app_route_data is None:
|
|
|
|
|
emit_reverse_proxy(base, upstream_path, indent=" ")
|
|
|
|
|
lines.append("}")
|
2026-03-14 22:56:47 -03:00
|
|
|
lines.append("")
|
|
|
|
|
continue
|
|
|
|
|
|
2026-03-16 09:49:17 -03:00
|
|
|
route_list = sorted(app_route_data.get("routes", []), key=lambda route: len(route.get("path_prefix", "")), reverse=True)
|
|
|
|
|
for route in route_list:
|
|
|
|
|
path_prefix = route.get("path_prefix", "/")
|
|
|
|
|
policy_type = get_policy_type(route.get("policy"))
|
|
|
|
|
matcher = f"{path_prefix}*"
|
2026-03-14 22:56:47 -03:00
|
|
|
if policy_type == "bypass":
|
2026-03-16 09:49:17 -03:00
|
|
|
lines.append(handle_open(matcher))
|
|
|
|
|
emit_reverse_proxy(base, upstream_path)
|
|
|
|
|
lines.append(" }")
|
|
|
|
|
lines.append("")
|
2026-03-14 22:56:47 -03:00
|
|
|
elif policy_type == "deny":
|
2026-03-16 09:49:17 -03:00
|
|
|
lines.append(handle_open(matcher))
|
|
|
|
|
lines.append(" respond 403")
|
|
|
|
|
lines.append(" }")
|
|
|
|
|
lines.append("")
|
2026-03-14 22:56:47 -03:00
|
|
|
else:
|
2026-03-16 09:49:17 -03:00
|
|
|
emit_protected_handle(matcher, base, upstream_path)
|
|
|
|
|
|
|
|
|
|
default_policy_type = get_policy_type(app_route_data.get("default_policy"))
|
|
|
|
|
if default_policy_type == "bypass":
|
|
|
|
|
emit_reverse_proxy(base, upstream_path, indent=" ")
|
|
|
|
|
elif default_policy_type == "deny":
|
|
|
|
|
lines.append(" respond 403")
|
|
|
|
|
else:
|
|
|
|
|
emit_protected_handle("*", base, upstream_path)
|
|
|
|
|
lines.append("}")
|
|
|
|
|
lines.append("")
|
2026-03-14 22:56:47 -03:00
|
|
|
|
2026-03-16 09:49:17 -03:00
|
|
|
return "\n".join(lines)
|
2026-03-14 22:56:47 -03:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def main():
|
|
|
|
|
apps = collect_all_applications()
|
|
|
|
|
auth_policies = load_json("auth_policies.json")
|
|
|
|
|
routes = load_json("routes.json")
|
|
|
|
|
|
|
|
|
|
caddyfile_content = build_caddyfile(apps, auth_policies, routes)
|
|
|
|
|
os.makedirs(os.path.dirname(CADDYFILE_PATH), exist_ok=True)
|
|
|
|
|
with open(CADDYFILE_PATH, "w", encoding="utf-8") as caddyfile:
|
|
|
|
|
caddyfile.write(caddyfile_content)
|
|
|
|
|
print(f"Caddyfile written to {CADDYFILE_PATH}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
main()
|