diff --git a/cf.py b/cf.py
new file mode 100644
index 0000000..c1ed1e4
--- /dev/null
+++ b/cf.py
@@ -0,0 +1,256 @@
+#!/usr/bin/env python3
+
+import os
+import sys
+import yaml
+import requests
+import subprocess
+from datetime import datetime
+from loguru import logger
+from dotenv import load_dotenv
+from pathlib import Path
+
+# Constants
+SCRIPT_DIR = Path(__file__).resolve().parent
+CADDYFILE_PATH = "/etc/caddy/Caddyfile"
+CF_DOMAINS_FILE = SCRIPT_DIR / 'cf_domains.yaml'
+ENV_FILE = SCRIPT_DIR / '.env'
+LOG_FILE = SCRIPT_DIR / 'cf_script.log'
+
+# Load environment variables
+load_dotenv(ENV_FILE)
+
+# Configure logger
+logger.remove() # Remove default handler
+logger.add(sys.stderr, format="{time:YYYY-MM-DD HH:mm:ss} {level} {message}", level="INFO", colorize=True)
+logger.add(LOG_FILE, format="{time:YYYY-MM-DD HH:mm:ss} {level} {message}", rotation="10 MB", retention="1 week", level="DEBUG")
+
+def get_current_ip():
+ return requests.get('https://am.i.mullvad.net/ip').text.strip()
+
+def update_env_ip(ip):
+ env_vars = {}
+ if ENV_FILE.exists():
+ with open(ENV_FILE, 'r') as f:
+ for line in f:
+ if '=' in line:
+ key, value = line.strip().split('=', 1)
+ env_vars[key] = value
+
+ env_vars['CURRENT_IP'] = ip
+
+ with open(ENV_FILE, 'w') as f:
+ for key, value in env_vars.items():
+ f.write(f"{key}={value}\n")
+
+def sort_yaml():
+ with open(CF_DOMAINS_FILE, 'r') as file:
+ data = yaml.safe_load(file)
+
+ sorted_data = dict(sorted(data.items()))
+
+ for domain, subdomains in sorted_data.items():
+ if isinstance(subdomains, dict):
+ sorted_data[domain] = dict(sorted(subdomains.items()))
+
+ with open(CF_DOMAINS_FILE, 'w') as file:
+ yaml.dump(sorted_data, file, sort_keys=False)
+
+def usage():
+ logger.error("Incorrect usage")
+ print("Usage: cf [--ip ] --port ")
+ print(" cf ddns [--force]")
+ print(" cf all [--force]")
+ sys.exit(1)
+
+def update_caddyfile(full_domain, caddy_ip, port):
+ with open(CADDYFILE_PATH, 'a') as file:
+ file.write(f"""
+{full_domain} {{
+ reverse_proxy {caddy_ip}:{port}
+ tls {{
+ dns cloudflare {{env.CLOUDFLARE_API_TOKEN}}
+ }}
+}}
+""")
+ logger.info(f"Configuration appended to {CADDYFILE_PATH} with correct formatting.")
+
+def update_dns_record(zone_id, dns_id, name, ip):
+ url = f'https://api.cloudflare.com/client/v4/zones/{zone_id}/dns_records/{dns_id}'
+ headers = {
+ 'Authorization': f'Bearer {os.getenv("CLOUDFLARE_API_TOKEN")}',
+ 'Content-Type': 'application/json'
+ }
+ data = {
+ 'type': 'A',
+ 'name': name,
+ 'content': ip,
+ 'ttl': 120,
+ 'proxied': True
+ }
+ response = requests.put(url, headers=headers, json=data)
+ return response.json()
+
+def ddns(force=False):
+ current_ip = get_current_ip()
+ last_ip = os.getenv('CURRENT_IP')
+
+ if current_ip != last_ip or force:
+ update_env_ip(current_ip)
+
+ with open(CF_DOMAINS_FILE, 'r') as f:
+ domains = yaml.safe_load(f)
+
+ for domain, data in domains.items():
+ zone_id = data['_id']
+ for subdomain, dns_id in data.items():
+ if subdomain == '_id':
+ continue
+ full_domain = f"{subdomain}.{domain}" if subdomain != '@' else domain
+ logger.info(f"Updating {full_domain}")
+ result = update_dns_record(zone_id, dns_id, full_domain, current_ip)
+ if result.get('success'):
+ logger.info(f"Successfully updated {full_domain}")
+ else:
+ logger.error(f"Failed to update {full_domain}: {result.get('errors', 'Unknown error')}")
+
+ logger.info(f"DDNS update completed at {datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')} UTC")
+ else:
+ logger.info("IP address hasn't changed. No updates needed.")
+
+def get_existing_record(zone_id, name):
+ url = f"https://api.cloudflare.com/client/v4/zones/{zone_id}/dns_records?type=A&name={name}"
+ headers = {
+ "Authorization": f"Bearer {os.getenv('CLOUDFLARE_API_TOKEN')}",
+ "Content-Type": "application/json"
+ }
+ response = requests.get(url, headers=headers)
+ result = response.json()
+ if result.get('success') and result['result']:
+ return result['result'][0]
+ return None
+
+def update_or_create_record(zone_id, subdomain, domain, cloudflare_ip):
+ # For root domain, we need to use the full domain name instead of "@"
+ full_name = domain if subdomain == "@" else f"{subdomain}.{domain}"
+
+ existing_record = get_existing_record(zone_id, full_name)
+
+ headers = {
+ "Authorization": f"Bearer {os.getenv('CLOUDFLARE_API_TOKEN')}",
+ "Content-Type": "application/json"
+ }
+ data = {
+ "type": "A",
+ "name": full_name,
+ "content": cloudflare_ip,
+ "ttl": 120,
+ "proxied": True
+ }
+
+ if existing_record:
+ # Check if the existing record has the same content
+ if existing_record['content'] == cloudflare_ip:
+ logger.info(f"Record for {full_name} already exists with the correct IP. No update needed.")
+ return {"success": True, "result": existing_record}
+
+ url = f"https://api.cloudflare.com/client/v4/zones/{zone_id}/dns_records/{existing_record['id']}"
+ response = requests.put(url, headers=headers, json=data)
+ else:
+ url = f"https://api.cloudflare.com/client/v4/zones/{zone_id}/dns_records"
+ response = requests.post(url, headers=headers, json=data)
+
+ result = response.json()
+
+ # Handle the specific error code
+ if not result.get('success') and result.get('errors'):
+ for error in result['errors']:
+ if error.get('code') == 81058: # "A record with the same settings already exists"
+ logger.info(f"Record for {full_name} already exists with the correct settings. No update needed.")
+ return {"success": True, "result": existing_record or data}
+
+ return result
+
+def main():
+ if len(sys.argv) < 2:
+ usage()
+
+ if sys.argv[1] in ['ddns', 'all']:
+ force = '--force' in sys.argv
+ ddns(force)
+ if sys.argv[1] == 'ddns':
+ return
+
+ if os.geteuid() != 0 and sys.argv[1] not in ['ddns', 'all']:
+ logger.error("This script must be run as root for Caddyfile modifications. Try using 'sudo'.")
+ sys.exit(1)
+
+ full_domain = sys.argv[1]
+ caddy_ip = "localhost"
+ port = None
+
+ i = 2
+ while i < len(sys.argv):
+ if sys.argv[i] in ['--ip', '-i']:
+ caddy_ip = sys.argv[i+1]
+ i += 2
+ elif sys.argv[i] in ['--port', '-p']:
+ port = sys.argv[i+1]
+ i += 2
+ else:
+ usage()
+
+ if not port:
+ usage()
+
+ parts = full_domain.split('.')
+ if len(parts) == 2:
+ domain = full_domain
+ subdomain = "@"
+ else:
+ subdomain = parts[0]
+ domain = '.'.join(parts[1:])
+
+ with open(CF_DOMAINS_FILE, 'r') as file:
+ cf_domains = yaml.safe_load(file)
+
+ if domain not in cf_domains:
+ logger.error(f"Domain {domain} not found in cf_domains.yaml")
+ sys.exit(1)
+
+ zone_id = cf_domains[domain].get('_id')
+ if not zone_id:
+ logger.error(f"Zone ID for {domain} could not be found.")
+ sys.exit(1)
+
+ cloudflare_ip = os.getenv('CURRENT_IP')
+
+ result = update_or_create_record(zone_id, subdomain, domain, cloudflare_ip)
+
+ if result.get('success'):
+ record_id = result['result'].get('id')
+ if record_id:
+ if subdomain == "@":
+ cf_domains[domain]["@"] = record_id
+ else:
+ cf_domains[domain][subdomain] = record_id
+ with open(CF_DOMAINS_FILE, 'w') as file:
+ yaml.dump(cf_domains, file)
+ logger.info(f"A record for {full_domain} created/updated and cf_domains.yaml updated successfully.")
+ else:
+ logger.info(f"A record for {full_domain} already exists with correct settings. No update needed.")
+ sort_yaml()
+ logger.info("YAML file sorted after update.")
+ update_caddyfile(full_domain, caddy_ip, port)
+ else:
+ error_message = result.get('errors', [{'message': 'Unknown error', 'code': 'N/A'}])[0]
+ logger.error(f"Failed to create/update A record for {full_domain}. Error: {error_message.get('message')} (Code: {error_message.get('code')})")
+
+ sort_yaml()
+ logger.info("YAML file sorted at end.")
+
+ logger.info("Restarting caddy!")
+ subprocess.run(['sudo', 'systemctl', 'restart', 'caddy'], check=True)
+
+if __name__ == "__main__":
+ main()
\ No newline at end of file