191 lines
6.5 KiB
Python
Executable file
191 lines
6.5 KiB
Python
Executable file
#!/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<domains>[^\s{]+(?:,\s*[^\s{]+)*)\s*{.*?reverse_proxy\s+(?P<target>https?:\/\/[\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)
|