#!/usr/bin/env python3 import os import re def get_user_input(prompt, default=None): """Interactive prompt with default values""" value = input(f"{prompt} [{default}]: ") or default return value def find_caddyfile(): """Check if the Caddyfile exists, otherwise ask for the path""" default_path = "./conf/Caddyfile" if os.path.exists(default_path): return default_path print("⚠ No Caddyfile found!") while True: custom_path = get_user_input("Enter the path to your Caddyfile") if os.path.exists(custom_path): return custom_path print("❌ File not found, please try again.") def parse_existing_entries(caddyfile_path): """Parse the existing Caddyfile to extract all configured domains""" existing_entries = {} try: with open(caddyfile_path, "r") as file: content = file.read() # Regex to find domain blocks and associated reverse proxy targets pattern = re.compile(r"(?P[^\s{]+(?:,\s*[^\s{]+)*)\s*{.*?reverse_proxy\s+(?Phttps?:\/\/[\d\.]+:\d+|[\d\.]+:\d+).*?}", re.DOTALL) matches = pattern.findall(content) for domains, target in matches: for domain in domains.split(", "): existing_entries[domain] = target.strip() except Exception as e: print(f"❌ Error reading Caddyfile: {e}") return existing_entries def format_caddy_entry(domains, target_ip, target_port, proxy_type): """Generate a properly formatted Caddy entry based on proxy type""" domain_list = ", ".join(domains) # Multiple domains in a single line if proxy_type == "standard": return f""" {domain_list} {{ tls {{ dns cloudflare {{env.CLOUDFLARE_API_TOKEN}} }} reverse_proxy {target_ip}:{target_port} }} """ elif proxy_type == "https_skip_verify": return f""" {domain_list} {{ tls {{ dns cloudflare {{env.CLOUDFLARE_API_TOKEN}} }} reverse_proxy https://{target_ip}:{target_port} {{ transport http {{ tls tls_insecure_skip_verify }} }} }} """ elif proxy_type == "opnsense": return f""" {domain_list} {{ tls {{ dns cloudflare {{env.CLOUDFLARE_API_TOKEN}} }} reverse_proxy https://{target_ip}:{target_port} {{ transport http {{ tls tls_insecure_skip_verify versions h1.1 # Enforce HTTP/1.1 }} header_up Host {{host}} header_up X-Real-IP {{remote_host}} header_up X-Forwarded-Proto {{scheme}} header_up X-Forwarded-For {{remote}} header_down Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" # Remove problematic headers header_up -Connection header_up -Upgrade }} }} """ def update_existing_entry(caddyfile_path, domain, new_entry): """Replace an existing entry for the given domain""" try: with open(caddyfile_path, "r") as file: content = file.read() # Find and replace the domain entry pattern = re.compile(rf"({domain}\s*\{{.*?reverse_proxy\s+[^\n]+\n.*?\}})", re.DOTALL) content = pattern.sub(new_entry.strip(), content) with open(caddyfile_path, "w") as file: file.write(content) print(f"✅ Updated entry for {domain}") except Exception as e: print(f"❌ Error updating Caddyfile: {e}") def add_caddy_entry(caddyfile_path): """Add new Caddy reverse proxy entries, showing existing entries first""" existing_entries = parse_existing_entries(caddyfile_path) print("\n📌 Existing Caddy Entries:") if existing_entries: for domain, target in existing_entries.items(): print(f" 🔹 {domain} → {target}") else: print(" ⚠ No entries found.") while True: domain = get_user_input("\nEnter the domain you want to configure", "") if not domain: print("❌ No domain provided. Skipping entry.") continue # If domain exists, extract its current values if domain in existing_entries: print(f"⚠ The domain {domain} already exists.") edit_existing = get_user_input("Do you want to edit this entry? (y/n)", "y").lower() == "y" if not edit_existing: continue existing_target = existing_entries[domain] existing_ip, existing_port = existing_target.replace("https://", "").replace("http://", "").split(":") else: existing_ip, existing_port = "192.168.1.100", "8080" target_ip = get_user_input("Enter the target IP", existing_ip) target_port = get_user_input("Enter the target port", existing_port) print("\nChoose the proxy mode:") print("1️⃣ Standard (No HTTPS changes)") print("2️⃣ Internal HTTPS (skip verify)") print("3️⃣ OPNsense Mode (skip verify + enforce HTTP/1.1)") # Pre-fill proxy type if editing mode_choice_default = "1" if "https://" in existing_entries.get(domain, "") and "tls_insecure_skip_verify" in existing_entries.get(domain, ""): mode_choice_default = "3" if "versions h1.1" in existing_entries.get(domain, "") else "2" mode_choice = get_user_input("Enter option (1/2/3)", mode_choice_default) proxy_type = "standard" if mode_choice == "2": proxy_type = "https_skip_verify" elif mode_choice == "3": proxy_type = "opnsense" new_entry = format_caddy_entry([domain], target_ip, target_port, proxy_type) if domain in existing_entries: update_existing_entry(caddyfile_path, domain, new_entry) else: try: with open(caddyfile_path, "a") as file: file.write(new_entry) print(f"\n✅ New entry added: {domain} → {target_ip}:{target_port}") except Exception as e: print(f"\n❌ Error writing to Caddyfile: {e}") return # Ask if another entry should be added more_entries = get_user_input("\nDo you want to add or edit another entry? (y/n)", "n").lower() == "y" if not more_entries: break # Restart Caddy container restart_caddy = get_user_input("\nDo you want to restart the Caddy container? (y/n)", "y").lower() == "y" if restart_caddy: os.system("docker compose restart caddy") print("🔄 Caddy container restarted!") if __name__ == "__main__": caddyfile_path = find_caddyfile() add_caddy_entry(caddyfile_path)