#!/usr/bin/env python3 """ Reads JSON config files exported by Django and generates /etc/caddy/Caddyfile. 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 from urllib.parse import urlparse JSON_DIR = os.environ.get("JSON_DIR", "/caddy_json_export") CADDYFILE_PATH = os.environ.get("CADDYFILE_PATH", "/etc/caddy/Caddyfile") 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") AUTH_GATEWAY_CHECK_URI = "/auth/check" 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 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 def build_caddyfile(apps, auth_policies, routes): policies = auth_policies.get("policies", {}) if auth_policies else {} route_entries = routes.get("entries", {}) if routes else {} lines = [] def get_policy_type(policy_name): if policy_name and policy_name in policies: return policies[policy_name].get("policy_type", "bypass") return "deny" 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(" }") lines.append("") for app in apps: hosts = app.get("hosts", []) upstream = app.get("upstream", "") static_routes = app.get("static_routes", []) app_id = app.get("id", "") if not hosts or not upstream: continue base, upstream_path = split_upstream(upstream) lines.append(f"{', '.join(hosts)} {{") emit_auth_portal() 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}") lines.append(" file_server") if cache_control: lines.append(f" header Cache-Control \"{cache_control}\"") lines.append(" }") lines.append("") app_route_data = route_entries.get(app_id) if app_route_data is None: emit_reverse_proxy(base, upstream_path, indent=" ") lines.append("}") lines.append("") continue 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}*" if policy_type == "bypass": lines.append(handle_open(matcher)) emit_reverse_proxy(base, upstream_path) lines.append(" }") lines.append("") elif policy_type == "deny": lines.append(handle_open(matcher)) lines.append(" respond 403") lines.append(" }") lines.append("") else: 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("") return "\n".join(lines) 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()