← back to Ga Allsites

dns_point.py

119 lines

#!/usr/bin/env python3
"""Point domains' apex(+www) A records at Kamatera (45.61.58.125), DNS-only.
Handles Cloudflare (API) + GoDaddy (API). Skips CF-Worker-bound apexes.
Idempotent + resumable. Prints a READY list (domains now pointing at Kamatera,
safe to cert) and a SKIP list. Usage: python3 dns_point.py d1.com d2.com ..."""
import json
import os
import pathlib
import subprocess
import sys
import urllib.error
import urllib.request

IP = "45.61.58.125"
ENV = pathlib.Path.home() / "Projects" / "secrets-manager" / ".env"


def ev(*ks):
    for k in ks:
        if os.environ.get(k):
            return os.environ[k]
    for line in ENV.read_text().splitlines():
        line = line.strip()
        for k in ks:
            if line.startswith(k + "="):
                return line.split("=", 1)[1].strip().strip('"').strip("'")
    return ""


CF = ev("CLOUDFLARE_API_TOKEN", "CF_API_TOKEN")
GK, GS = ev("GODADDY_API_KEY"), ev("GODADDY_API_SECRET")


def cf(url, method="GET", payload=None):
    req = urllib.request.Request(url, method=method,
        data=json.dumps(payload).encode() if payload else None,
        headers={"Authorization": f"Bearer {CF}", "Content-Type": "application/json"})
    try:
        return json.loads(urllib.request.urlopen(req, timeout=30).read())
    except urllib.error.HTTPError as e:
        return {"__err__": e.code, "__body__": e.read().decode()[:200]}


def ns_host(d):
    try:
        out = subprocess.run(["dig", "+short", "NS", d], capture_output=True, text=True, timeout=15).stdout.lower()
    except Exception:
        out = ""
    if "cloudflare" in out:
        return "cf"
    if "domaincontrol" in out:
        return "godaddy"
    return "other"


def point_cf(d):
    z = cf(f"https://api.cloudflare.com/client/v4/zones?name={d}").get("result")
    if not z:
        return "SKIP no-cf-zone"
    zid = z[0]["id"]
    for name in (d, f"www.{d}"):
        recs = cf(f"https://api.cloudflare.com/client/v4/zones/{zid}/dns_records?name={name}").get("result", [])
        # remove parking placeholders / conflicting CNAME so an A can live here
        for r in recs:
            if (r["type"] == "AAAA" and r["content"] in ("100::", "::")) or r["type"] == "CNAME":
                cf(f"https://api.cloudflare.com/client/v4/zones/{zid}/dns_records/{r['id']}", "DELETE")
        a = [r for r in recs if r["type"] == "A"]
        body = {"type": "A", "name": name, "content": IP, "ttl": 300, "proxied": False}
        if a:
            res = cf(f"https://api.cloudflare.com/client/v4/zones/{zid}/dns_records/{a[0]['id']}", "PUT", body)
        else:
            res = cf(f"https://api.cloudflare.com/client/v4/zones/{zid}/dns_records", "POST", body)
        if res.get("__err__"):
            body_txt = res.get("__body__", "")
            if "Workers" in body_txt:
                return "SKIP cf-worker"
            if name == d:  # apex failure is fatal for this domain
                return f"ERR cf {res['__err__']}"
    return "READY"


def point_godaddy(d):
    if not (GK and GS):
        return "SKIP no-godaddy-creds"
    hdr = {"Authorization": f"sso-key {GK}:{GS}", "Content-Type": "application/json"}
    body = json.dumps([{"data": IP, "ttl": 600, "type": "A", "name": "@"}]).encode()
    req = urllib.request.Request(f"https://api.godaddy.com/v1/domains/{d}/records/A/%40",
                                 data=body, headers=hdr, method="PUT")
    try:
        urllib.request.urlopen(req, timeout=30).read()
    except urllib.error.HTTPError as e:
        return f"ERR gd {e.code}"
    # www (best-effort; ignore failure)
    try:
        wb = json.dumps([{"data": IP, "ttl": 600, "type": "A", "name": "www"}]).encode()
        urllib.request.urlopen(urllib.request.Request(
            f"https://api.godaddy.com/v1/domains/{d}/records/A/www", data=wb, headers=hdr, method="PUT"), timeout=30).read()
    except Exception:
        pass
    return "READY"


def main():
    doms = sys.argv[1:]
    ready, skip = [], []
    for d in doms:
        host = ns_host(d)
        r = point_cf(d) if host == "cf" else point_godaddy(d) if host == "godaddy" else "SKIP other-registrar"
        (ready if r == "READY" else skip).append(d)
        print(f"  {d:34} [{host}] {r}")
    print(f"\nREADY={len(ready)}  SKIP={len(skip)}")
    print("READY_LIST:" + " ".join(ready))
    if skip:
        print("SKIP_LIST:" + " ".join(skip))


if __name__ == "__main__":
    main()