#!/usr/bin/env python3 """Configure Cloudflare tunnel + DNS for otantik.cm without wiping existing hostnames.""" from __future__ import annotations import json import os import sys import urllib.error import urllib.parse import urllib.request TOKEN = os.environ.get("CF_TOKEN", "").strip() ACCOUNT_HINT = "67432c1959c4bc5272137831e5f2d4d9" TUNNEL_HINT = "3d6fc119-d147-4954-9015-fd75b8f43ed8" HOSTNAMES = [ "otantik.cm", "www.otantik.cm", "app.otantik.cm", "wifi.otantik.cm", "pay.otantik.cm", ] def cf(method: str, path: str, body: dict | None = None): url = "https://api.cloudflare.com/client/v4" + path data = None if body is None else json.dumps(body).encode() req = urllib.request.Request( url, data=data, method=method, headers={ "Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json", }, ) try: with urllib.request.urlopen(req, timeout=45) as resp: raw = resp.read().decode() except urllib.error.HTTPError as e: raw = e.read().decode("utf-8", "replace") print("HTTP", e.code, path) print(raw[:2000]) raise SystemExit(1) parsed = json.loads(raw) if not parsed.get("success"): print("API error", path, json.dumps(parsed.get("errors"), indent=2)[:2000]) raise SystemExit(1) return parsed.get("result") def main() -> None: if not TOKEN: raise SystemExit("missing CF_TOKEN") print("=== verify ===") v = cf("GET", "/user/tokens/verify") print(v) print("=== accounts ===") accounts = cf("GET", "/accounts?per_page=50") or [] for a in accounts: print(a.get("id"), a.get("name")) account_id = ACCOUNT_HINT ids = [a.get("id") for a in accounts] if ACCOUNT_HINT not in ids and accounts: account_id = accounts[0]["id"] print("using account", account_id) print("=== zones otantik.cm ===") zones = cf("GET", "/zones?name=otantik.cm") or [] if not zones: print("zone missing — listing first 20 zones") allz = cf("GET", "/zones?per_page=20") or [] for z in allz: print(z.get("name"), z.get("id"), z.get("status")) raise SystemExit("otantik.cm is not in this Cloudflare account yet") zone = zones[0] zone_id = zone["id"] print("zone", zone.get("name"), zone_id, zone.get("status"), zone.get("name_servers")) print("=== tunnels ===") tunnels = cf("GET", f"/accounts/{account_id}/cfd_tunnel?is_deleted=false") or [] for t in tunnels: print(t.get("id"), t.get("name"), t.get("status")) tunnel_id = TUNNEL_HINT tids = [t.get("id") for t in tunnels] if TUNNEL_HINT not in tids: # pick a healthy named tunnel on this account live = [t for t in tunnels if t.get("status") in ("healthy", "down", None)] if tunnels: tunnel_id = tunnels[0]["id"] print("hint tunnel not listed, using", tunnel_id, tunnels[0].get("name")) print("=== current ingress ===") cfg = cf("GET", f"/accounts/{account_id}/cfd_tunnel/{tunnel_id}/configurations") or {} config = cfg.get("config") or {} ingress = list(config.get("ingress") or []) print(json.dumps(ingress, indent=2)[:4000]) catchall = None kept = [] existing_hosts = set() for rule in ingress: if "hostname" not in rule: catchall = rule continue existing_hosts.add(rule["hostname"]) kept.append(rule) origin = { "service": "http://127.0.0.1:80", "originRequest": {"connectTimeout": 30, "httpHostHeader": "", "noTLSVerify": True}, } added = [] for host in HOSTNAMES: if host in existing_hosts: print("already present", host) continue kept.append({"hostname": host, **origin}) added.append(host) if catchall is None: catchall = {"service": "http_status:404"} new_ingress = kept + [catchall] new_config = dict(config) new_config["ingress"] = new_ingress print("=== putting ingress, adding", added, "===") cf( "PUT", f"/accounts/{account_id}/cfd_tunnel/{tunnel_id}/configurations", {"config": new_config}, ) cname_target = f"{tunnel_id}.cfargotunnel.com" print("=== DNS ===") records = cf("GET", f"/zones/{zone_id}/dns_records?per_page=100") or [] by_name = {r.get("name"): r for r in records} for host in HOSTNAMES: rec = by_name.get(host) payload = { "type": "CNAME", "name": host, "content": cname_target, "proxied": True, "ttl": 1, } if rec: if rec.get("type") == "CNAME" and rec.get("content") == cname_target and rec.get("proxied"): print("dns ok", host) continue print("updating", host, rec.get("type"), rec.get("content")) cf("PATCH", f"/zones/{zone_id}/dns_records/{rec['id']}", payload) else: print("creating", host) cf("POST", f"/zones/{zone_id}/dns_records", payload) print("DONE") print("tunnel", tunnel_id) print("zone_status", zone.get("status")) print("ns", zone.get("name_servers")) if __name__ == "__main__": main()