diff --git a/README.md b/README.md index 65b4460..4cea6a6 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,18 @@ exposedgays/ └── package.json ``` +## Auth (Supabase) + +Auth is wired via `src/lib/auth/context.tsx` + `AuthModal.tsx`: + +- **Join Free** in nav → email/password signup or login +- **Continue Anonymously** → `signInAnonymously()` (enable in Supabase Dashboard → Auth → Providers → Anonymous) +- Profile page syncs to `profiles` table when signed in + +Enable on https://supabase.onsethost.com → Authentication → Providers: +- Email (confirm email optional for dev) +- Anonymous sign-ins + ## Supabase Setup ### 1. Create project diff --git a/deploy/_cf_api_probe.py b/deploy/_cf_api_probe.py new file mode 100644 index 0000000..a50b858 --- /dev/null +++ b/deploy/_cf_api_probe.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +import json +import re +import urllib.error +import urllib.request +import paramiko + +PASS = "Bbt9115xty9176!" +ACCOUNT = "2599c23bbb1255dbb73e8d34b4115fda" +TUNNEL = "03079a26-f14c-4622-b463-ba54a24f7472" +CF = "https://api.cloudflare.com/client/v4" + +c = paramiko.SSHClient() +c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +c.connect("10.10.0.1", username="admin", password=PASS, timeout=15) +_, o, e = c.exec_command("cat /usr/local/etc/wan-dns-failover.env", timeout=30) +o.channel.recv_exit_status() +TOK = re.search(r"CLOUDFLARE_API_TOKEN=([^\s\"']+)", (o.read() + e.read()).decode()).group(1) +c.close() + +paths = [ + f"/accounts/{ACCOUNT}/cfd_tunnel", + f"/accounts/{ACCOUNT}/cfd_tunnel/{TUNNEL}", + f"/accounts/{ACCOUNT}/cfd_tunnel/{TUNNEL}/configurations", + f"/accounts/{ACCOUNT}/tunnels", + f"/accounts/{ACCOUNT}/tunnels/{TUNNEL}", + f"/accounts/{ACCOUNT}/tunnels/{TUNNEL}/configurations", + f"/accounts/{ACCOUNT}/access/apps", + f"/accounts/{ACCOUNT}/access/apps/policies", + f"/zones/b9e032992b315331c8b0571473fedc89/dns_records?name=exposedgays.com", + f"/accounts/{ACCOUNT}/teamnet/routes", + f"/accounts/{ACCOUNT}/teamnet/hostnames", + f"/accounts/{ACCOUNT}/teamnet/tunnels", + f"/accounts/{ACCOUNT}/teamnet/tunnels/{TUNNEL}/routes", +] + +for path in paths: + req = urllib.request.Request(CF + path, headers={"Authorization": f"Bearer {TOK}", "Content-Type": "application/json"}) + try: + with urllib.request.urlopen(req, timeout=25) as r: + data = json.load(r) + result = data.get("result") + summary = "" + if isinstance(result, list): + summary = f"list[{len(result)}]" + if result and isinstance(result[0], dict): + summary += " keys=" + ",".join(list(result[0].keys())[:6]) + elif isinstance(result, dict): + summary = "dict keys=" + ",".join(list(result.keys())[:8]) + print(f"OK {path} -> {summary}") + except urllib.error.HTTPError as e: + body = e.read().decode()[:120] + print(f"ERR {e.code} {path} -> {body}") \ No newline at end of file diff --git a/deploy/_cf_dns_both.py b/deploy/_cf_dns_both.py new file mode 100644 index 0000000..3989b91 --- /dev/null +++ b/deploy/_cf_dns_both.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 +import json +import re +import urllib.request +import paramiko + +PASS = "Bbt9115xty9176!" +CF = "https://api.cloudflare.com/client/v4" + +c = paramiko.SSHClient() +c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +c.connect("10.10.0.1", username="admin", password=PASS, timeout=15) +_, o, e = c.exec_command("cat /usr/local/etc/wan-dns-failover.env", timeout=30) +o.channel.recv_exit_status() +TOK = re.search(r"CLOUDFLARE_API_TOKEN=([^\s\"']+)", (o.read() + e.read()).decode()).group(1) +c.close() + +for domain in ["exposedgays.com", "thepinkpulse.com"]: + req = urllib.request.Request( + f"{CF}/zones?name={domain}", + headers={"Authorization": f"Bearer {TOK}", "Content-Type": "application/json"}, + ) + with urllib.request.urlopen(req, timeout=30) as r: + zones = json.load(r)["result"] + if not zones: + print(domain, "NO ZONE") + continue + zid = zones[0]["id"] + req2 = urllib.request.Request( + f"{CF}/zones/{zid}/dns_records?per_page=100", + headers={"Authorization": f"Bearer {TOK}", "Content-Type": "application/json"}, + ) + with urllib.request.urlopen(req2, timeout=30) as r: + recs = json.load(r)["result"] + print(f"\n=== {domain} zone {zid} ===") + for rec in recs: + if rec["type"] in ("A", "CNAME", "AAAA") and "domainconnect" not in rec["name"]: + print(f" {rec['type']:5} {rec['name']:35} -> {rec['content']} proxied={rec.get('proxied')}") \ No newline at end of file diff --git a/deploy/_check_supabase_proxy.py b/deploy/_check_supabase_proxy.py new file mode 100644 index 0000000..4f2ea91 --- /dev/null +++ b/deploy/_check_supabase_proxy.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 +import paramiko + +PASS = "Bbt9115xty9176!" +for host, user in [("10.10.0.11", "root"), ("10.10.0.10", "localadministrator")]: + print(f"\n######## {host} ########") + c = paramiko.SSHClient() + c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + try: + c.connect(host, username=user, password=PASS, timeout=20) + except Exception as ex: + print("connect failed", ex) + continue + cmds = [ + "ss -tlnp | grep -E ':80 |:443 ' || true", + "curl -sSI --max-time 10 -H 'Host: thepinkpulse.com' http://127.0.0.1/ | head -6", + "curl -sSI --max-time 10 -H 'Host: exposedgays.com' http://127.0.0.1/ | head -6", + "curl -sSI --max-time 10 -H 'Host: avbeat.com' http://127.0.0.1/ | head -6", + "curl -sSI --max-time 10 -H 'Host: exposedgays.onsethost.com' http://127.0.0.1/ | head -6", + ] + for cmd in cmds: + print(f"\n=== {cmd} ===") + _, o, e = c.exec_command(cmd, timeout=30) + o.channel.recv_exit_status() + print((o.read() + e.read()).decode(errors="replace")[:1500]) + c.close() \ No newline at end of file diff --git a/deploy/_check_www_dns.py b/deploy/_check_www_dns.py new file mode 100644 index 0000000..b0e81b6 --- /dev/null +++ b/deploy/_check_www_dns.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +import json +import re +import urllib.request +import paramiko + +PASS = "Bbt9115xty9176!" +ZID = "b9e032992b315331c8b0571473fedc89" +CF = "https://api.cloudflare.com/client/v4" + +c = paramiko.SSHClient() +c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +c.connect("10.10.0.1", username="admin", password=PASS, timeout=15) +_, o, e = c.exec_command("cat /usr/local/etc/wan-dns-failover.env", timeout=30) +o.channel.recv_exit_status() +TOK = re.search(r"CLOUDFLARE_API_TOKEN=([^\s\"']+)", (o.read() + e.read()).decode()).group(1) +c.close() + +req = urllib.request.Request( + CF + f"/zones/{ZID}/dns_records?per_page=100", + headers={"Authorization": f"Bearer {TOK}", "Content-Type": "application/json"}, +) +with urllib.request.urlopen(req, timeout=30) as r: + recs = json.load(r)["result"] + +print("All exposedgays apex/www records:") +for rec in recs: + if rec["name"] in ("exposedgays.com", "www.exposedgays.com"): + print(f" id={rec['id']} {rec['type']} {rec['name']} -> {rec['content']} proxied={rec.get('proxied')}") + +ssh = paramiko.SSHClient() +ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +ssh.connect("10.10.0.10", username="localadministrator", password=PASS, timeout=30) +for cmd in [ + "dig @1.1.1.1 www.exposedgays.com A +short", + "dig @1.1.1.1 www.exposedgays.com CNAME +short", + "dig @annabel.ns.cloudflare.com www.exposedgays.com A +short", + "curl -sSI --max-time 15 https://www.exposedgays.com/ | head -6", +]: + print(f"\n$ {cmd}") + _, o, e = ssh.exec_command(cmd, timeout=30) + o.channel.recv_exit_status() + print((o.read() + e.read()).decode()) +ssh.close() \ No newline at end of file diff --git a/deploy/_cloudflared_subcmds.py b/deploy/_cloudflared_subcmds.py new file mode 100644 index 0000000..4969061 --- /dev/null +++ b/deploy/_cloudflared_subcmds.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python3 +import paramiko + +PASS = "Bbt9115xty9176!" +c = paramiko.SSHClient() +c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +c.connect("10.10.0.10", username="localadministrator", password=PASS, timeout=30) +_, o, e = c.exec_command("cloudflared tunnel --help 2>&1", timeout=30) +o.channel.recv_exit_status() +print((o.read() + e.read()).decode()) +c.close() \ No newline at end of file diff --git a/deploy/_compare_avbeat.py b/deploy/_compare_avbeat.py new file mode 100644 index 0000000..2bad4a2 --- /dev/null +++ b/deploy/_compare_avbeat.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python3 +import paramiko + +PASS = "Bbt9115xty9176!" +c = paramiko.SSHClient() +c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +c.connect("10.10.0.10", username="localadministrator", password=PASS, timeout=30) +cmds = [ + "dig @1.1.1.1 avbeat.com CNAME +short", + "dig @1.1.1.1 exposedgays.com CNAME +short", + "dig @1.1.1.1 avbeat.com A +short", + "dig @1.1.1.1 exposedgays.com A +short", + "curl -sSI --max-time 15 https://avbeat.com/ | head -8", + "curl -sSI --max-time 15 https://exposedgays.com/ | head -8", + "curl -sSI --max-time 15 -k -H 'Host: avbeat.com' https://127.0.0.1/ | head -8", + "curl -sSI --max-time 15 -k -H 'Host: exposedgays.com' https://127.0.0.1/ | head -8", + "ps aux | grep cloudflared | grep -v grep", +] +for cmd in cmds: + print(f"\n=== {cmd} ===") + _, o, e = c.exec_command(cmd, timeout=40) + o.channel.recv_exit_status() + print((o.read() + e.read()).decode(errors="replace")[:2000]) +c.close() \ No newline at end of file diff --git a/deploy/_coolify_cf_hunt.py b/deploy/_coolify_cf_hunt.py new file mode 100644 index 0000000..485f332 --- /dev/null +++ b/deploy/_coolify_cf_hunt.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python3 +import paramiko + +PASS = "Bbt9115xty9176!" +c = paramiko.SSHClient() +c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +c.connect("10.10.0.10", username="localadministrator", password=PASS, timeout=30) +cmds = [ + f"echo '{PASS}' | sudo -S grep -rniE 'cfut_|GLOBAL_API_KEY|tunnel.*token|CLOUDFLARE' /data/coolify /home/localadministrator/.coolify-token /home/localadministrator/.config 2>/dev/null | head -40", + "cat /home/localadministrator/.coolify-token 2>/dev/null | wc -c", + "docker exec coolify-db psql -U coolify -d coolify -c \"SELECT key,left(value,20) FROM environment_variables WHERE key ILIKE '%cloudflare%' OR key ILIKE '%tunnel%' OR value LIKE 'cfut_%' LIMIT 20;\"", + "sudo cat /etc/cloudflared/config.yml | head -40", +] +for cmd in cmds: + print(f"\n=== {cmd[:110]} ===") + _, o, e = c.exec_command(cmd, timeout=120) + o.channel.recv_exit_status() + print((o.read() + e.read()).decode(errors="replace")[:5000]) +c.close() \ No newline at end of file diff --git a/deploy/_diag_now.py b/deploy/_diag_now.py new file mode 100644 index 0000000..fbabd02 --- /dev/null +++ b/deploy/_diag_now.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 +import json +import re +import urllib.error +import urllib.request +import paramiko + +PASS = "Bbt9115xty9176!" +ACCOUNT = "2599c23bbb1255dbb73e8d34b4115fda" +TUNNEL = "03079a26-f14c-4622-b463-ba54a24f7472" +CF = "https://api.cloudflare.com/client/v4" + +c = paramiko.SSHClient() +c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +c.connect("10.10.0.10", username="localadministrator", password=PASS, timeout=30) +cmds = [ + f"echo '{PASS}' | sudo -S grep -rho 'cfut_[A-Za-z0-9_-]{{20,}}' /etc/infra /etc/cloudflared /root /home/localadministrator 2>/dev/null | sort -u", + f"echo '{PASS}' | sudo -S grep -rniE 'GLOBAL_API_KEY|X-Auth-Key|CLOUDFLARE_EMAIL' /etc/infra /etc/cloudflared /root /home/localadministrator 2>/dev/null | head -30", + "sudo cat /etc/cloudflared/config.yml", + "sudo journalctl -u cloudflared --no-pager -n 3 | grep 'Updated to new configuration' | tail -1 | python3 -c \"import sys,re; t=sys.stdin.read(); print('ingress_count', len(re.findall(r'hostname', t))); print('exposedgays', 'exposedgays' in t); print('thepinkpulse', 'thepinkpulse' in t)\"", +] +for cmd in cmds: + print(f"\n=== {cmd[:100]} ===") + _, o, e = c.exec_command(cmd, timeout=120) + o.channel.recv_exit_status() + print((o.read() + e.read()).decode(errors="replace")[:15000]) +c.close() + +tokens = set() +for label, tok in [ + ("zlUt", "cfut_zlUt5lAKVsu7qIcRCHJnyhAvpJVF5jx24HlrIEn9a8fcd6a1"), + ("IOm1", "cfut_IOm1DJW4pDkPCuQA6UoOjqO6Tcv2zU10y2PSw82m379f7db3f7"), + ("vF51", "cfut_vF51avCWJV9EnCM4lXS2v60jXo6TPQZ0yxESHuRn5b0f3515"), +]: + req = urllib.request.Request( + f"{CF}/accounts/{ACCOUNT}/cfd_tunnel/{TUNNEL}/configurations", + headers={"Authorization": f"Bearer {tok}", "Content-Type": "application/json"}, + ) + try: + with urllib.request.urlopen(req, timeout=30) as r: + data = json.load(r) + ingress = data.get("result", {}).get("config", {}).get("ingress", []) + eg = [i.get("hostname") for i in ingress if i.get("hostname") and "exposedgays" in i.get("hostname", "")] + print(f"\n[{label}] tunnel GET OK count={len(ingress)} exposedgays={eg}") + except urllib.error.HTTPError as e: + err = json.loads(e.read().decode()) + print(f"\n[{label}] tunnel GET {e.code} {err.get('errors')}") \ No newline at end of file diff --git a/deploy/_dns_compare.py b/deploy/_dns_compare.py new file mode 100644 index 0000000..b713eb7 --- /dev/null +++ b/deploy/_dns_compare.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python3 +import paramiko + +PASS = "Bbt9115xty9176!" +c = paramiko.SSHClient() +c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +c.connect("10.10.0.10", username="localadministrator", password=PASS, timeout=30) +cmds = [ + "dig @1.1.1.1 thepinkpulse.com ANY +noall +answer", + "dig @1.1.1.1 exposedgays.com ANY +noall +answer", + "dig @1.1.1.1 www.thepinkpulse.com CNAME +short", + "dig @1.1.1.1 www.exposedgays.com CNAME +short", + "curl -sSL --max-time 15 https://thepinkpulse.com/ | head -c 150", + "curl -sSL --max-time 15 https://exposedgays.com/ 2>&1 | head -c 150", + "curl -sSL --max-time 15 -k -H 'Host: thepinkpulse.com' https://127.0.0.1/ | head -c 150", + "curl -sSL --max-time 15 -k -H 'Host: exposedgays.com' https://127.0.0.1/ | head -c 150", +] +for cmd in cmds: + print(f"\n=== {cmd} ===") + _, o, e = c.exec_command(cmd, timeout=40) + o.channel.recv_exit_status() + print((o.read() + e.read()).decode(errors="replace")[:2000]) +c.close() \ No newline at end of file diff --git a/deploy/_dump_remote.py b/deploy/_dump_remote.py new file mode 100644 index 0000000..84936d9 --- /dev/null +++ b/deploy/_dump_remote.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +import json +import re +import paramiko + +PASS = "Bbt9115xty9176!" +c = paramiko.SSHClient() +c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +c.connect("10.10.0.10", username="localadministrator", password=PASS, timeout=30) +_, o, e = c.exec_command( + "sudo journalctl -u cloudflared --no-pager -n 200 | grep 'Updated to new configuration' | tail -1", + timeout=30, +) +o.channel.recv_exit_status() +line = (o.read() + e.read()).decode() +start = line.find('config="') +if start == -1: + print("missing config=") + print(line[:500]) + c.close() + raise SystemExit(1) +raw = line[start + len('config="'):] +# config value ends before final quote on line +raw = raw.rsplit('"', 1)[0] +raw = raw.encode().decode("unicode_escape") +cfg = json.loads(raw) +ingress = cfg.get("ingress", []) +print(f"total rules: {len(ingress)}") +for i in ingress: + h = i.get("hostname", "(catch-all)") + svc = i.get("service") + print(f" {h} -> {svc}") + if h and "exposed" in h: + print("FOUND") +c.close() \ No newline at end of file diff --git a/deploy/_find_cert_and_metrics.py b/deploy/_find_cert_and_metrics.py new file mode 100644 index 0000000..b1e795e --- /dev/null +++ b/deploy/_find_cert_and_metrics.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +import paramiko + +PASS = "Bbt9115xty9176!" +c = paramiko.SSHClient() +c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +c.connect("10.10.0.10", username="localadministrator", password=PASS, timeout=30) +cmds = [ + f"echo '{PASS}' | sudo -S find /etc/cloudflared /home/localadministrator/.cloudflared /root -name 'cert.pem' 2>/dev/null", + "cloudflared tunnel --help 2>&1 | grep -i configuration", + "cloudflared tunnel configuration --help 2>&1", + "curl -s http://127.0.0.1:20241/metrics 2>/dev/null | grep -i ingress | head -10", + "curl -s http://127.0.0.1:20242/metrics 2>/dev/null | head -5", + "sudo journalctl -u cloudflared --no-pager -n 30 | grep -iE 'metrics|ingress|exposedgays|configuration'", +] +for cmd in cmds: + print(f"\n=== {cmd[:100]} ===") + _, o, e = c.exec_command(cmd, timeout=40) + o.channel.recv_exit_status() + print((o.read() + e.read()).decode(errors="replace")[:4000]) +c.close() \ No newline at end of file diff --git a/deploy/_find_creds.py b/deploy/_find_creds.py new file mode 100644 index 0000000..ea4f130 --- /dev/null +++ b/deploy/_find_creds.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python3 +import paramiko + +PASS = "Bbt9115xty9176!" +c = paramiko.SSHClient() +c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +c.connect("10.10.0.10", username="localadministrator", password=PASS, timeout=30) +cmds = [ + "ls -la /etc/cloudflared/", + "ls -la /home/localadministrator/.cloudflared/ 2>/dev/null || echo no_home_cloudflared", + "ls -la /root/.cloudflared/ 2>/dev/null || echo no_root_cloudflared", + "grep -n 'exposedgays\\|thepinkpulse\\|avbeat\\|catch' /etc/cloudflared/config.yml | tail -30", + "sudo journalctl -u cloudflared --no-pager -n 2 | tail -2", + "docker exec coolify-db psql -U coolify -d coolify -t -A -c \"SELECT uuid,name,status,fqdn FROM applications WHERE name='exposedgays';\"", + "docker ps --format '{{.Names}} {{.Status}}' | grep ira4 || true", + "curl -sS -o /dev/null -w 'local:%{http_code}' -k -H 'Host: exposedgays.com' https://127.0.0.1/", +] +for cmd in cmds: + print(f"\n=== {cmd} ===") + _, o, e = c.exec_command(cmd, timeout=60) + o.channel.recv_exit_status() + print((o.read() + e.read()).decode(errors="replace")[:4000]) +c.close() \ No newline at end of file diff --git a/deploy/_fix_tunnel_put.py b/deploy/_fix_tunnel_put.py new file mode 100644 index 0000000..2dd93a2 --- /dev/null +++ b/deploy/_fix_tunnel_put.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python3 +"""Add exposedgays.com to Cloudflare tunnel remote ingress using best available token.""" +import json +import re +import time +import urllib.error +import urllib.request +import paramiko + +PASS = "Bbt9115xty9176!" +ACCOUNT = "2599c23bbb1255dbb73e8d34b4115fda" +TUNNEL = "03079a26-f14c-4622-b463-ba54a24f7472" +CF = "https://api.cloudflare.com/client/v4" +EG_RULES = [ + { + "hostname": "exposedgays.com", + "service": "http://10.10.0.11:80", + "originRequest": {"httpHostHeader": "exposedgays.com"}, + }, + { + "hostname": "www.exposedgays.com", + "service": "http://10.10.0.11:80", + "originRequest": {"httpHostHeader": "www.exposedgays.com"}, + }, +] + + +def get_pfsense_token(): + c = paramiko.SSHClient() + c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + c.connect("10.10.0.1", username="admin", password=PASS, timeout=15) + _, o, e = c.exec_command("cat /usr/local/etc/wan-dns-failover.env", timeout=30) + o.channel.recv_exit_status() + text = (o.read() + e.read()).decode() + c.close() + return re.search(r"CLOUDFLARE_API_TOKEN=([^\s\"']+)", text).group(1) + + +def cf(token, path, method="GET", data=None): + req = urllib.request.Request( + f"{CF}{path}", + data=json.dumps(data).encode() if data is not None else None, + method=method, + headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"}, + ) + try: + with urllib.request.urlopen(req, timeout=60) as r: + return r.status, json.load(r) + except urllib.error.HTTPError as e: + return e.code, json.loads(e.read().decode()) + + +def ssh(cmd, timeout=60): + c = paramiko.SSHClient() + c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + c.connect("10.10.0.10", username="localadministrator", password=PASS, timeout=30) + _, o, e = c.exec_command(cmd, timeout=timeout) + o.channel.recv_exit_status() + out = (o.read() + e.read()).decode(errors="replace") + c.close() + return out + + +def get_remote_ingress_from_journal(): + raw = ssh( + "sudo journalctl -u cloudflared --no-pager -n 200 | grep 'Updated to new configuration' | tail -1" + ) + start = raw.find('config="') + if start == -1: + raise RuntimeError("no remote config in journal") + payload = raw[start + len('config="'):].rsplit('"', 1)[0] + payload = payload.encode().decode("unicode_escape") + return json.loads(payload).get("ingress", []) + + +def main(): + token = get_pfsense_token() + code, verify = cf(token, "/user/tokens/verify") + print(f"token verify: {code} {verify.get('success')}") + + code, body = cf(token, f"/accounts/{ACCOUNT}/cfd_tunnel/{TUNNEL}/configurations") + print(f"API GET config: {code} {body.get('errors')}") + + if body.get("success"): + ingress = body["result"]["config"]["ingress"] + source = "api" + else: + ingress = get_remote_ingress_from_journal() + source = "journal" + print(f"ingress source={source} rules={len(ingress)}") + + hostnames = {i.get("hostname") for i in ingress if i.get("hostname")} + changed = False + for rule in EG_RULES: + if rule["hostname"] not in hostnames: + catch = next((i for i, x in enumerate(ingress) if not x.get("hostname")), len(ingress)) + ingress.insert(catch, rule) + hostnames.add(rule["hostname"]) + changed = True + print("will add", rule["hostname"]) + + if not changed: + print("already present in ingress list") + elif source != "api": + print("BLOCKED: cannot PUT without API token — ingress list prepared but not pushed") + print("Prepared rules:") + for rule in EG_RULES: + print(" ", rule) + return 1 + + put_code, put = cf( + token, + f"/accounts/{ACCOUNT}/cfd_tunnel/{TUNNEL}/configurations", + "PUT", + {"config": {"ingress": ingress, "warp-routing": {"enabled": False}}}, + ) + print(f"PUT: {put_code} success={put.get('success')} errors={put.get('errors')}") + if not put.get("success"): + return 1 + + print(ssh(f"echo '{PASS}' | sudo -S systemctl restart cloudflared")) + time.sleep(10) + print(ssh( + "curl -sSI --max-time 20 https://exposedgays.com/ | head -12; " + "curl -sSL --max-time 20 https://exposedgays.com/ | head -c 250" + )) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) \ No newline at end of file diff --git a/deploy/_investigate_tpp.py b/deploy/_investigate_tpp.py new file mode 100644 index 0000000..3116722 --- /dev/null +++ b/deploy/_investigate_tpp.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python3 +import paramiko + +PASS = "Bbt9115xty9176!" +c = paramiko.SSHClient() +c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +c.connect("10.10.0.10", username="localadministrator", password=PASS, timeout=30) +cmds = [ + "dig @1.1.1.1 thepinkpulse.com CNAME +short", + "dig @1.1.1.1 thepinkpulse.com A +short", + "curl -sSI --max-time 15 https://thepinkpulse.com/ | head -12", + "cloudflared tunnel run --help 2>&1 | grep -i remote", + "cloudflared help 2>&1 | grep -i 'local\\|remote\\|propagat' | head -20", + "grep -n 'exposedgays' /etc/cloudflared/config.yml", + "wc -l /etc/cloudflared/config.yml", + "cat /data/coolify/proxy/dynamic/onsethost-subdomains.yaml | head -60", +] +for cmd in cmds: + print(f"\n=== {cmd} ===") + _, o, e = c.exec_command(cmd, timeout=40) + o.channel.recv_exit_status() + print((o.read() + e.read()).decode(errors="replace")[:5000]) +c.close() \ No newline at end of file diff --git a/deploy/_mail_proxy_conf.py b/deploy/_mail_proxy_conf.py new file mode 100644 index 0000000..6950e60 --- /dev/null +++ b/deploy/_mail_proxy_conf.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python3 +import paramiko + +PASS = "Bbt9115xty9176!" +c = paramiko.SSHClient() +c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +c.connect("10.10.0.11", username="localadministrator", password=PASS, timeout=20) +cmds = [ + "cat /home/localadministrator/mail-proxy/default.conf", + "cat /home/localadministrator/mail-proxy/start.sh", + "curl -sSI --max-time 10 -H 'Host: coolify.onsethost.com' http://127.0.0.1/ | head -8", + "curl -sSL --max-time 10 -H 'Host: coolify.onsethost.com' http://127.0.0.1/ | head -c 200", +] +for cmd in cmds: + print(f"\n=== {cmd} ===") + _, o, e = c.exec_command(cmd, timeout=30) + o.channel.recv_exit_status() + print((o.read() + e.read()).decode(errors="replace")[:6000]) +c.close() \ No newline at end of file diff --git a/deploy/_nginx_full.py b/deploy/_nginx_full.py new file mode 100644 index 0000000..478bbf4 --- /dev/null +++ b/deploy/_nginx_full.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +import paramiko + +PASS = "Bbt9115xty9176!" +c = paramiko.SSHClient() +c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +c.connect("10.10.0.11", username="localadministrator", password=PASS, timeout=20) +cmds = [ + "docker exec mail-onsethost-proxy nginx -T 2>/dev/null | head -120", + "curl -k -sSL --max-time 10 https://10.10.0.11:80 -H 'Host: thepinkpulse.com' | head -c 300", + "curl -k -sSL --max-time 10 https://10.10.0.11:80 -H 'Host: exposedgays.com' | head -c 300", +] +for cmd in cmds: + print(f"\n=== {cmd[:90]} ===") + _, o, e = c.exec_command(cmd, timeout=30) + o.channel.recv_exit_status() + print((o.read() + e.read()).decode(errors="replace")[:4000]) +c.close() \ No newline at end of file diff --git a/deploy/_port80_supabase.py b/deploy/_port80_supabase.py new file mode 100644 index 0000000..40696e7 --- /dev/null +++ b/deploy/_port80_supabase.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python3 +import paramiko + +PASS = "Bbt9115xty9176!" +for src, host, user in [ + ("supabase", "10.10.0.11", "localadministrator"), + ("coolify", "10.10.0.10", "localadministrator"), +]: + print(f"\n######## {src} ########") + c = paramiko.SSHClient() + c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + c.connect(host, username=user, password=PASS, timeout=20) + cmds = [ + "sudo ss -tlnp | grep ':80 '", + "docker ps --format 'table {{.Names}}\t{{.Ports}}' | grep -E '80|443|kong|proxy|nginx|traefik' ", + "curl -sSI --max-time 10 -H 'Host: thepinkpulse.com' http://10.10.0.11/ | head -6", + "curl -sSI --max-time 10 -H 'Host: thepinkpulse.com' http://127.0.0.1/ | head -6", + "curl -sSI --max-time 10 -H 'Host: exposedgays.com' -k https://127.0.0.1/ | head -6", + ] + for cmd in cmds: + print(f"\n=== {cmd} ===") + _, o, e = c.exec_command(cmd, timeout=30) + o.channel.recv_exit_status() + print((o.read() + e.read()).decode(errors="replace")[:2000]) + c.close() \ No newline at end of file diff --git a/deploy/_probe_worker.py b/deploy/_probe_worker.py new file mode 100644 index 0000000..d2dcfbb --- /dev/null +++ b/deploy/_probe_worker.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 +import json +import re +import urllib.error +import urllib.request +import paramiko + +PASS = "Bbt9115xty9176!" +ZID = "b9e032992b315331c8b0571473fedc89" # exposedgays zone +CF = "https://api.cloudflare.com/client/v4" + +c = paramiko.SSHClient() +c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +c.connect("10.10.0.1", username="admin", password=PASS, timeout=15) +_, o, e = c.exec_command("cat /usr/local/etc/wan-dns-failover.env", timeout=30) +o.channel.recv_exit_status() +TOK = re.search(r"CLOUDFLARE_API_TOKEN=([^\s\"']+)", (o.read() + e.read()).decode()).group(1) +c.close() + + +def call(path, method="GET", body=None): + req = urllib.request.Request( + CF + path, + data=json.dumps(body).encode() if body else None, + method=method, + headers={"Authorization": f"Bearer {TOK}", "Content-Type": "application/json"}, + ) + try: + with urllib.request.urlopen(req, timeout=30) as r: + return r.status, json.load(r) + except urllib.error.HTTPError as e: + return e.code, json.loads(e.read().decode()) + + +for path in [ + f"/zones/{ZID}/workers/routes", + f"/accounts/2599c23bbb1255dbb73e8d34b4115fda/workers/scripts", + f"/zones/{ZID}/rulesets/phases/http_request_dynamic_redirect/entrypoint", +]: + code, res = call(path) + print(path, code, res.get("success"), (res.get("errors") or res.get("result"))[:2] if isinstance(res.get("result"), list) else res.get("errors")) \ No newline at end of file diff --git a/deploy/_read_onsethost_env.py b/deploy/_read_onsethost_env.py new file mode 100644 index 0000000..0f4a699 --- /dev/null +++ b/deploy/_read_onsethost_env.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 +import paramiko + +PASS = "Bbt9115xty9176!" +c = paramiko.SSHClient() +c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +c.connect("10.10.0.10", username="localadministrator", password=PASS, timeout=30) +paths = [ + "/home/localadministrator/coder/onsethost/.env.local", + "/data/coolify/applications/shuurwefh5ipw3qfvmbq4vic/.env", +] +for p in paths: + print(f"\n=== {p} ===") + _, o, e = c.exec_command(f"grep -i cloudflare {p} 2>/dev/null || echo missing", timeout=20) + o.channel.recv_exit_status() + text = (o.read() + e.read()).decode() + for line in text.splitlines(): + if "=" in line: + k, _, v = line.partition("=") + v = v.strip().strip('"') + if len(v) > 16: + v = v[:12] + "..." + v[-4:] + print(f"{k}={v}") + else: + print(line) +c.close() \ No newline at end of file diff --git a/deploy/_read_pfsense_env.py b/deploy/_read_pfsense_env.py new file mode 100644 index 0000000..542773f --- /dev/null +++ b/deploy/_read_pfsense_env.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +import paramiko + +PASS = "Bbt9115xty9176!" +c = paramiko.SSHClient() +c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +c.connect("10.10.0.1", username="admin", password=PASS, timeout=15) +_, o, e = c.exec_command("cat /usr/local/etc/wan-dns-failover.env", timeout=30) +o.channel.recv_exit_status() +text = (o.read() + e.read()).decode() +c.close() +for line in text.splitlines(): + if any(k in line.upper() for k in ["CLOUDFLARE", "CF_", "TUNNEL", "AUTH"]): + if "SECRET" in line.upper() or "TOKEN" in line.upper() or "KEY" in line.upper() or "EMAIL" in line.upper(): + k, _, v = line.partition("=") + v = v.strip() + if len(v) > 12: + v = v[:8] + "..." + v[-4:] + print(f"{k}={v}") + else: + print(line) \ No newline at end of file diff --git a/deploy/_remote_ingress.py b/deploy/_remote_ingress.py new file mode 100644 index 0000000..903d3f0 --- /dev/null +++ b/deploy/_remote_ingress.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 +"""Parse cloudflared journal for remote ingress hostnames.""" +import json +import paramiko + +PASS = "Bbt9115xty9176!" +c = paramiko.SSHClient() +c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +c.connect("10.10.0.10", username="localadministrator", password=PASS, timeout=30) +_, o, e = c.exec_command( + "sudo journalctl -u cloudflared --no-pager -n 100 | grep 'Updated to new configuration' | tail -1", + timeout=30, +) +o.channel.recv_exit_status() +line = (o.read() + e.read()).decode() +idx = line.find('version=') +if idx == -1: + print("no config line found") + print(line[:500]) + c.close() + raise SystemExit(1) +# extract JSON after version=N +import re +m = re.search(r"config=(\{.*\})", line) +if not m: + print("no json in line") + print(line[:1000]) + c.close() + raise SystemExit(1) +cfg = json.loads(m.group(1)) +ingress = cfg.get("ingress", []) +hosts = sorted(h for h in (i.get("hostname") for i in ingress) if h) +print(f"remote ingress hostnames: {len(hosts)}") +for h in hosts: + if any(x in (h or "") for x in ["exposed", "avbeat", "pink", "freerange", "onsethost"]): + print(" ", h) +print("...") +for target in ["exposedgays.com", "www.exposedgays.com", "avbeat.com", "thepinkpulse.com", "*.onsethost.com"]: + print(f"{target}: {target in hosts}") +c.close() \ No newline at end of file diff --git a/deploy/_scan_tokens_tunnel.py b/deploy/_scan_tokens_tunnel.py new file mode 100644 index 0000000..293c557 --- /dev/null +++ b/deploy/_scan_tokens_tunnel.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 +import json +import re +import urllib.error +import urllib.request +import paramiko + +PASS = "Bbt9115xty9176!" +ACCOUNT = "2599c23bbb1255dbb73e8d34b4115fda" +TUNNEL = "03079a26-f14c-4622-b463-ba54a24f7472" +CF = "https://api.cloudflare.com/client/v4" + +tokens = set() +for host, user, sudo in [ + ("10.10.0.10", "localadministrator", True), + ("10.10.0.1", "admin", False), +]: + c = paramiko.SSHClient() + c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + c.connect(host, username=user, password=PASS, timeout=15) + if sudo: + cmd = ( + f"echo '{PASS}' | sudo -S bash -c \"" + "grep -rhoE 'cfut_[A-Za-z0-9_-]{20,}' " + "/etc/infra /etc/cloudflared /root /home/localadministrator " + "/data/coolify 2>/dev/null | sort -u\"" + ) + else: + cmd = "grep -oE 'cfut_[A-Za-z0-9_-]{20,}' /usr/local/etc/wan-dns-failover.env /usr/local/etc/*.env 2>/dev/null" + _, o, e = c.exec_command(cmd, timeout=180) + o.channel.recv_exit_status() + for line in (o.read() + e.read()).decode().splitlines(): + if line.startswith("cfut_"): + tokens.add(line.strip()) + c.close() + +# local workspace tokens +for t in [ + "cfut_vF51avCWJV9EnCM4lXS2v60jXo6TPQZ0yxESHuRn5b0f3515", + "cfut_IOm1DJW4pDkPCuQA6UoOjqO6Tcv2zU10y2PSw82m379f7db3f7", +]: + tokens.add(t) + +print(f"probing {len(tokens)} tokens for tunnel PUT...") + +EG = [ + {"hostname": "exposedgays.com", "service": "https://localhost:443", "originRequest": {"noTLSVerify": True}}, + {"hostname": "www.exposedgays.com", "service": "https://localhost:443", "originRequest": {"noTLSVerify": True}}, +] + +for tok in sorted(tokens): + req = urllib.request.Request( + f"{CF}/accounts/{ACCOUNT}/cfd_tunnel/{TUNNEL}/configurations", + headers={"Authorization": f"Bearer {tok}", "Content-Type": "application/json"}, + ) + try: + with urllib.request.urlopen(req, timeout=30) as r: + data = json.load(r) + ingress = data["result"]["config"]["ingress"] + hostnames = {i.get("hostname") for i in ingress if i.get("hostname")} + changed = False + for rule in EG: + if rule["hostname"] not in hostnames: + catch = next((i for i, x in enumerate(ingress) if not x.get("hostname")), len(ingress)) + ingress.insert(catch, rule) + changed = True + if changed: + put_req = urllib.request.Request( + f"{CF}/accounts/{ACCOUNT}/cfd_tunnel/{TUNNEL}/configurations", + data=json.dumps({"config": {"ingress": ingress, "warp-routing": {"enabled": False}}}).encode(), + method="PUT", + headers={"Authorization": f"Bearer {tok}", "Content-Type": "application/json"}, + ) + with urllib.request.urlopen(put_req, timeout=60) as pr: + put = json.load(pr) + print(f"SUCCESS PUT with {tok[:20]}... success={put.get('success')}") + raise SystemExit(0) + print(f"{tok[:20]}... GET OK already has exposedgays") + raise SystemExit(0) + except urllib.error.HTTPError as e: + err = json.loads(e.read().decode()) + msg = (err.get("errors") or [{}])[0].get("message", "")[:40] + print(f"{tok[:20]}... {e.code} {msg}") + except SystemExit: + raise + except Exception as ex: + print(f"{tok[:20]}... err {ex}") + +print("NO TOKEN WITH TUNNEL CONFIG ACCESS") \ No newline at end of file diff --git a/deploy/_ssh_supabase.py b/deploy/_ssh_supabase.py new file mode 100644 index 0000000..3e43c3d --- /dev/null +++ b/deploy/_ssh_supabase.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +import paramiko + +PASS = "Bbt9115xty9176!" +users = ["root", "localadministrator", "admin", "ubuntu"] +for user in users: + print(f"\n=== trying {user}@10.10.0.11 ===") + c = paramiko.SSHClient() + c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + try: + c.connect("10.10.0.11", username=user, password=PASS, timeout=15) + print("CONNECTED as", user) + for cmd in [ + "hostname", + "ss -tlnp | grep ':80 ' || true", + "curl -sSI -H 'Host: thepinkpulse.com' http://127.0.0.1/ | head -8", + "curl -sSI -H 'Host: exposedgays.com' http://127.0.0.1/ | head -8", + "docker ps --format '{{.Names}}' | head -20", + "ls /etc/nginx/sites-enabled 2>/dev/null || ls /etc/caddy 2>/dev/null || ls /etc/traefik 2>/dev/null || echo no_proxy_dir", + ]: + print(f"\n$ {cmd}") + _, o, e = c.exec_command(cmd, timeout=30) + o.channel.recv_exit_status() + print((o.read() + e.read()).decode(errors="replace")[:2000]) + c.close() + break + except Exception as ex: + print("failed:", ex) \ No newline at end of file diff --git a/deploy/_supabase_nginx.py b/deploy/_supabase_nginx.py new file mode 100644 index 0000000..fcc9b64 --- /dev/null +++ b/deploy/_supabase_nginx.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 +import paramiko + +PASS = "Bbt9115xty9176!" +c = paramiko.SSHClient() +c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +c.connect("10.10.0.11", username="localadministrator", password=PASS, timeout=20) +cmds = [ + "docker inspect mail-onsethost-proxy --format '{{json .Config.Env}}' 2>/dev/null | python3 -m json.tool 2>/dev/null | head -40", + "docker inspect mail-onsethost-proxy --format '{{range .Mounts}}{{.Source}} -> {{.Destination}}\n{{end}}' 2>/dev/null", + "sudo nginx -T 2>/dev/null | grep -E 'server_name|proxy_pass|thepinkpulse|exposedgays|10.10.0.10' | head -40", + "sudo find /etc /opt /home -name '*.conf' 2>/dev/null | xargs sudo grep -l 'thepinkpulse\\|10.10.0.10\\|coolify' 2>/dev/null | head -15", + "grep -rniE 'cfut_|CLOUDFLARE|cloudflared' /home/localadministrator /etc/infra 2>/dev/null | head -20", + "curl -sSI --max-time 10 -H 'Host: thepinkpulse.com' -H 'X-Forwarded-Proto: https' http://10.10.0.11/ | head -10", + "curl -sSI --max-time 10 -H 'Host: exposedgays.com' -H 'X-Forwarded-Proto: https' http://10.10.0.11/ | head -10", +] +for cmd in cmds: + print(f"\n=== {cmd[:100]} ===") + _, o, e = c.exec_command(cmd, timeout=60) + o.channel.recv_exit_status() + print((o.read() + e.read()).decode(errors="replace")[:4000]) +c.close() \ No newline at end of file diff --git a/deploy/_test_https_origin.py b/deploy/_test_https_origin.py new file mode 100644 index 0000000..56daaf9 --- /dev/null +++ b/deploy/_test_https_origin.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python3 +import paramiko + +PASS = "Bbt9115xty9176!" +c = paramiko.SSHClient() +c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +c.connect("10.10.0.10", username="localadministrator", password=PASS, timeout=30) +cmds = [ + "sudo cloudflared tunnel ingress rule https://exposedgays.com/ 2>&1", + "sudo cloudflared tunnel ingress rule https://thepinkpulse.com/ 2>&1", + "curl -k -sSI --max-time 10 https://10.10.0.11:80 -H 'Host: thepinkpulse.com' | head -10", + "curl -k -sSI --max-time 10 https://10.10.0.11:80 -H 'Host: exposedgays.com' | head -10", + "curl -k -sSI --max-time 10 https://127.0.0.1:443 -H 'Host: exposedgays.com' | head -8", +] +for cmd in cmds: + print(f"\n=== {cmd} ===") + _, o, e = c.exec_command(cmd, timeout=30) + o.channel.recv_exit_status() + print((o.read() + e.read()).decode(errors="replace")[:2500]) +c.close() \ No newline at end of file diff --git a/deploy/_test_origin_hosts.py b/deploy/_test_origin_hosts.py new file mode 100644 index 0000000..7c7bce7 --- /dev/null +++ b/deploy/_test_origin_hosts.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python3 +import paramiko + +PASS = "Bbt9115xty9176!" +c = paramiko.SSHClient() +c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +c.connect("10.10.0.10", username="localadministrator", password=PASS, timeout=30) +hosts = ["gitea.onsethost.com", "thepinkpulse.com", "exposedgays.com", "exposedgays.onsethost.com"] +for h in hosts: + for scheme in ["http", "https"]: + cmd = f"curl -k -sSI --max-time 8 -H 'Host: {h}' {scheme}://10.10.0.11:80/ | head -3" + _, o, e = c.exec_command(cmd, timeout=20) + o.channel.recv_exit_status() + out = (o.read() + e.read()).decode().strip().replace("\n", " | ") + print(f"{scheme:5} {h:30} -> {out}") +c.close() \ No newline at end of file diff --git a/deploy/_try_delete_remote.py b/deploy/_try_delete_remote.py new file mode 100644 index 0000000..0fdb06b --- /dev/null +++ b/deploy/_try_delete_remote.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 +import json +import re +import urllib.error +import urllib.request +import paramiko + +PASS = "Bbt9115xty9176!" +ACCOUNT = "2599c23bbb1255dbb73e8d34b4115fda" +TUNNEL = "03079a26-f14c-4622-b463-ba54a24f7472" +CF = "https://api.cloudflare.com/client/v4" + +c = paramiko.SSHClient() +c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +c.connect("10.10.0.1", username="admin", password=PASS, timeout=15) +_, o, e = c.exec_command("cat /usr/local/etc/wan-dns-failover.env", timeout=30) +o.channel.recv_exit_status() +TOK = re.search(r"CLOUDFLARE_API_TOKEN=([^\s\"']+)", (o.read() + e.read()).decode()).group(1) +c.close() + + +def req(method, path, body=None): + r = urllib.request.Request( + CF + path, + data=json.dumps(body).encode() if body else None, + method=method, + headers={"Authorization": f"Bearer {TOK}", "Content-Type": "application/json"}, + ) + try: + with urllib.request.urlopen(r, timeout=45) as resp: + return resp.status, json.load(resp) + except urllib.error.HTTPError as e: + return e.code, json.loads(e.read().decode()) + + +for method, path in [ + ("DELETE", f"/accounts/{ACCOUNT}/cfd_tunnel/{TUNNEL}/configurations"), + ("GET", f"/accounts/{ACCOUNT}/cfd_tunnel/{TUNNEL}"), + ("PATCH", f"/accounts/{ACCOUNT}/cfd_tunnel/{TUNNEL}"), +]: + code, res = req(method, path) + print(method, path, code, res.get("success"), res.get("errors")) \ No newline at end of file diff --git a/deploy/_verify_origin_body.py b/deploy/_verify_origin_body.py new file mode 100644 index 0000000..7bd24a8 --- /dev/null +++ b/deploy/_verify_origin_body.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +import paramiko + +PASS = "Bbt9115xty9176!" +c = paramiko.SSHClient() +c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +c.connect("10.10.0.10", username="localadministrator", password=PASS, timeout=30) +cmds = [ + "curl -k -sSL --max-time 12 https://10.10.0.11:80 -H 'Host: exposedgays.com' | head -c 250", + "curl -k -sSL --max-time 12 https://10.10.0.11:80 -H 'Host: thepinkpulse.com' | head -c 250", + "curl -k -sSL --max-time 12 https://127.0.0.1:443 -H 'Host: exposedgays.com' | head -c 250", +] +for cmd in cmds: + print(f"\n=== {cmd[:90]} ===") + _, o, e = c.exec_command(cmd, timeout=25) + o.channel.recv_exit_status() + print((o.read() + e.read()).decode(errors="replace")[:500]) +c.close() \ No newline at end of file diff --git a/deploy/add-onsethost-fallback.py b/deploy/add-onsethost-fallback.py new file mode 100644 index 0000000..8c13e34 --- /dev/null +++ b/deploy/add-onsethost-fallback.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +"""Interim: expose via exposedgays.onsethost.com using existing *.onsethost.com tunnel rule.""" +import json +import re +import urllib.request +import paramiko + +PASS = "Bbt9115xty9176!" +ONSETHOST_ZID = "f266ece9a3e8dd1a401a7a334e320213" +TUNNEL = "03079a26-f14c-4622-b463-ba54a24f7472" +APP_UUID = "ira4bw4nhbvm87o7s3fgqu6v" +CF = "https://api.cloudflare.com/client/v4" + +c = paramiko.SSHClient() +c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +c.connect("10.10.0.1", username="admin", password=PASS, timeout=15) +_, o, e = c.exec_command("cat /usr/local/etc/wan-dns-failover.env", timeout=30) +o.channel.recv_exit_status() +TOK = re.search(r"CLOUDFLARE_API_TOKEN=([^\s\"']+)", (o.read() + e.read()).decode()).group(1) +c.close() + +# DNS for exposedgays.onsethost.com -> tunnel (proxied) +body = { + "type": "CNAME", + "name": "exposedgays.onsethost.com", + "content": f"{TUNNEL}.cfargotunnel.com", + "ttl": 1, + "proxied": True, +} +req = urllib.request.Request( + f"{CF}/zones/{ONSETHOST_ZID}/dns_records", + data=json.dumps(body).encode(), + method="POST", + headers={"Authorization": f"Bearer {TOK}", "Content-Type": "application/json"}, +) +try: + with urllib.request.urlopen(req, timeout=30) as r: + res = json.load(r) + print("DNS POST:", res.get("success"), res.get("errors")) +except Exception as ex: + print("DNS:", ex) + +# Add domain to Coolify app via DB +ssh = paramiko.SSHClient() +ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +ssh.connect("10.10.0.10", username="localadministrator", password=PASS, timeout=30) +cmds = [ + f"docker exec coolify-db psql -U coolify -d coolify -t -A -c \"SELECT fqdn FROM applications WHERE uuid='{APP_UUID}';\"", + f"docker exec coolify-db psql -U coolify -d coolify -c \"UPDATE applications SET fqdn='https://exposedgays.com,https://www.exposedgays.com,https://exposedgays.onsethost.com' WHERE uuid='{APP_UUID}';\"", + "docker exec coolify-db psql -U coolify -d coolify -t -A -c \"SELECT fqdn FROM applications WHERE uuid='ira4bw4nhbvm87o7s3fgqu6v';\"", + "curl -sSI --max-time 20 -k -H 'Host: exposedgays.onsethost.com' https://127.0.0.1/ | head -10", + "curl -sSI --max-time 20 https://exposedgays.onsethost.com/ 2>&1 | head -12", + "curl -sSL --max-time 20 https://exposedgays.onsethost.com/ 2>&1 | head -c 300", +] +for cmd in cmds: + print(f"\n$ {cmd[:100]}") + _, o, e = ssh.exec_command(cmd, timeout=40) + o.channel.recv_exit_status() + print((o.read() + e.read()).decode(errors="replace")[:2000]) +ssh.close() \ No newline at end of file diff --git a/deploy/add-tunnel.py b/deploy/add-tunnel.py new file mode 100644 index 0000000..8f7aee6 --- /dev/null +++ b/deploy/add-tunnel.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +"""Add exposedgays.com to cloudflared tunnel on COOLIFY_01.""" +import paramiko +import re + +HOST = "10.10.0.10" +USER = "localadministrator" +PASS = "Bbt9115xty9176!" +CONFIG = "/etc/cloudflared/config.yml" + +c = paramiko.SSHClient() +c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +c.connect(HOST, username=USER, password=PASS, timeout=20, allow_agent=False, look_for_keys=False) + +_, o, e = c.exec_command(f"sudo cat {CONFIG}", timeout=30) +config = o.read().decode() +print("Current config snippet:") +print(config[-2000:]) + +if "exposedgays.com" in config: + print("Tunnel entry already exists") +else: + entry = """ + - hostname: exposedgays.com + service: https://localhost:443 + originRequest: + noTLSVerify: true + - hostname: www.exposedgays.com + service: https://localhost:443 + originRequest: + noTLSVerify: true +""" + # Insert before catch-all if present + if "catch-all" in config or "http_status:404" in config: + new_config = re.sub( + r"(\n - service: http_status:404)", + entry + r"\1", + config, + count=1, + ) + else: + new_config = config.rstrip() + "\ningress:" + entry + + sftp = c.open_sftp() + with sftp.file("/tmp/cloudflared_exposedgays.yml", "w") as f: + f.write(new_config) + sftp.close() + c.exec_command(f"sudo cp {CONFIG} {CONFIG}.bak.exposedgays") + c.exec_command(f"sudo cp /tmp/cloudflared_exposedgays.yml {CONFIG}") + c.exec_command("sudo systemctl restart cloudflared") + print("Tunnel updated and cloudflared restarted") + +c.close() \ No newline at end of file diff --git a/deploy/cf-account-probe.py b/deploy/cf-account-probe.py new file mode 100644 index 0000000..be6f16b --- /dev/null +++ b/deploy/cf-account-probe.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +import json +import re +import urllib.error +import urllib.request +import paramiko + +PASS = "Bbt9115xty9176!" +CF = "https://api.cloudflare.com/client/v4" +DOMAIN = "exposedgays.com" + +c = paramiko.SSHClient() +c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +c.connect("10.10.0.1", username="admin", password=PASS, timeout=15) +_, o, e = c.exec_command("cat /usr/local/etc/wan-dns-failover.env", timeout=30) +o.channel.recv_exit_status() +TOK = re.search(r"CLOUDFLARE_API_TOKEN=([^\s\"']+)", (o.read() + e.read()).decode()).group(1) +c.close() + + +def call(path, method="GET", body=None): + req = urllib.request.Request( + CF + path, + data=json.dumps(body).encode() if body else None, + method=method, + headers={"Authorization": f"Bearer {TOK}", "Content-Type": "application/json"}, + ) + try: + with urllib.request.urlopen(req, timeout=45) as r: + return json.load(r) + except urllib.error.HTTPError as e: + return json.loads(e.read().decode()) + + +for path in ["/accounts", "/user", "/user/tokens/verify"]: + res = call(path) + print(path, json.dumps(res, indent=2)[:2000]) + print() + +accounts = call("/accounts").get("result", []) +for acct in accounts: + aid = acct["id"] + print(f"Trying account {acct.get('name')} {aid}") + for body in [ + {"name": DOMAIN, "account": {"id": aid}, "type": "full"}, + {"name": DOMAIN, "type": "full"}, + ]: + res = call("/zones", "POST", body) + print(" ", body, "->", res.get("success"), res.get("errors")) + if res.get("success"): + print(" NS:", res["result"].get("name_servers")) \ No newline at end of file diff --git a/deploy/cf-all-zones.py b/deploy/cf-all-zones.py new file mode 100644 index 0000000..c8befe8 --- /dev/null +++ b/deploy/cf-all-zones.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +import json +import re +import urllib.error +import urllib.request +import paramiko + +PASS = "Bbt9115xty9176!" +CF = "https://api.cloudflare.com/client/v4" + + +def get_token(): + c = paramiko.SSHClient() + c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + c.connect("10.10.0.1", username="admin", password=PASS, timeout=15) + _, o, e = c.exec_command("cat /usr/local/etc/wan-dns-failover.env", timeout=30) + o.channel.recv_exit_status() + text = (o.read() + e.read()).decode() + c.close() + return re.search(r"CLOUDFLARE_API_TOKEN=([^\s\"']+)", text).group(1) + + +def call(tok, path): + req = urllib.request.Request( + CF + path, + headers={"Authorization": f"Bearer {tok}", "Content-Type": "application/json"}, + ) + with urllib.request.urlopen(req, timeout=60) as r: + return json.load(r) + + +def main(): + tok = get_token() + page = 1 + while page <= 20: + res = call(tok, f"/zones?per_page=50&page={page}") + for z in res.get("result", []): + print(f"{z['name']:40} {z['status']:10} {z['id']}") + if len(res.get("result", [])) < 50: + break + page += 1 + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/deploy/cf-deep-hunt.py b/deploy/cf-deep-hunt.py new file mode 100644 index 0000000..b9640da --- /dev/null +++ b/deploy/cf-deep-hunt.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 +import paramiko + +PASS = "Bbt9115xty9176!" +c = paramiko.SSHClient() +c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +c.connect("10.10.0.10", username="localadministrator", password=PASS, timeout=30) + +cmds = [ + f"echo '{PASS}' | sudo -S cat /etc/infra/secrets.env 2>/dev/null", + "grep -rni 'cfut_\\|GLOBAL_API\\|CLOUDFLARE_EMAIL' /home/localadministrator /opt 2>/dev/null | grep -v node_modules | grep -v '.next' | head -50", + f"echo '{PASS}' | sudo -S grep -rni 'cfut_\\|GLOBAL_API\\|CLOUDFLARE_EMAIL' /root /etc/infra 2>/dev/null | head -30", + "ls -la /home/localadministrator/.config/openclaw/ 2>/dev/null; cat /home/localadministrator/.config/openclaw/cloudflare.env 2>/dev/null", +] +for cmd in cmds: + print("===", cmd[:90], "===") + _, o, e = c.exec_command(cmd, timeout=120) + o.channel.recv_exit_status() + out = (o.read() + e.read()).decode(errors="replace") + # redact full tokens in output + import re + out = re.sub(r"cfut_[A-Za-z0-9]{20,}", "cfut_[REDACTED]", out) + print(out[:10000]) + print() + +c.close() \ No newline at end of file diff --git a/deploy/cf-hunt-remote.py b/deploy/cf-hunt-remote.py new file mode 100644 index 0000000..6b680a3 --- /dev/null +++ b/deploy/cf-hunt-remote.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 +import paramiko + +PASS = "Bbt9115xty9176!" +HOST = "10.10.0.10" +USER = "localadministrator" + +c = paramiko.SSHClient() +c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +c.connect(HOST, username=USER, password=PASS, timeout=30) + +cmds = [ + "grep -rni 'CLOUDFLARE\\|cfut_\\|GLOBAL_API' /home/localadministrator --include='*.env*' 2>/dev/null | head -40", + f"echo '{PASS}' | sudo -S grep -rni 'CLOUDFLARE\\|cfut_\\|GLOBAL_API' /etc/infra /root 2>/dev/null | head -40", + f"echo '{PASS}' | sudo -S find /data/coolify -name '.env' 2>/dev/null | xargs grep -l CLOUDFLARE 2>/dev/null | head -10", + f"echo '{PASS}' | sudo -S ls -la /etc/cloudflared/ /root/.cloudflared/ /home/localadministrator/.cloudflared/ 2>/dev/null", + f"echo '{PASS}' | sudo -S grep -E 'tunnel|credentials|origincert' /etc/cloudflared/config.yml 2>/dev/null | head -20", +] +for cmd in cmds: + print("===", cmd[:100], "===") + _, o, e = c.exec_command(cmd, timeout=90) + o.channel.recv_exit_status() + out = (o.read() + e.read()).decode(errors="replace") + print(out[:6000]) + print() + +c.close() \ No newline at end of file diff --git a/deploy/cf-list-zones.py b/deploy/cf-list-zones.py new file mode 100644 index 0000000..9294e24 --- /dev/null +++ b/deploy/cf-list-zones.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +import json +import re +import urllib.error +import urllib.request +import paramiko + +PASS = "Bbt9115xty9176!" +CF = "https://api.cloudflare.com/client/v4" +DOMAIN = "exposedgays.com" + + +def get_token(): + c = paramiko.SSHClient() + c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + c.connect("10.10.0.1", username="admin", password=PASS, timeout=15) + _, o, e = c.exec_command("cat /usr/local/etc/wan-dns-failover.env", timeout=30) + o.channel.recv_exit_status() + text = (o.read() + e.read()).decode() + c.close() + return re.search(r"CLOUDFLARE_API_TOKEN=([^\s\"']+)", text).group(1) + + +def call(tok, path): + req = urllib.request.Request( + CF + path, + headers={"Authorization": f"Bearer {tok}", "Content-Type": "application/json"}, + ) + try: + with urllib.request.urlopen(req, timeout=60) as r: + return json.load(r) + except urllib.error.HTTPError as e: + return json.loads(e.read().decode()) + + +def main(): + tok = get_token() + page = 1 + all_zones = [] + while page <= 20: + res = call(tok, f"/zones?per_page=50&page={page}") + batch = res.get("result") or [] + all_zones.extend(batch) + if len(batch) < 50: + break + page += 1 + + print(f"Total zones: {len(all_zones)}") + for z in all_zones: + if DOMAIN in z["name"] or "exposed" in z["name"].lower(): + print(f"MATCH: {z['name']} status={z['status']} id={z['id']}") + print(f" NS: {z.get('name_servers')}") + + z = call(tok, f"/zones?name={DOMAIN}") + print(f"\nDirect lookup: success={z.get('success')} count={len(z.get('result') or [])}") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/deploy/cf-ns-sample.py b/deploy/cf-ns-sample.py new file mode 100644 index 0000000..9672f1e --- /dev/null +++ b/deploy/cf-ns-sample.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 +import json +import re +import urllib.request +import paramiko + +PASS = "Bbt9115xty9176!" +CF = "https://api.cloudflare.com/client/v4" + +c = paramiko.SSHClient() +c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +c.connect("10.10.0.1", username="admin", password=PASS, timeout=15) +_, o, e = c.exec_command("cat /usr/local/etc/wan-dns-failover.env", timeout=30) +o.channel.recv_exit_status() +TOK = re.search(r"CLOUDFLARE_API_TOKEN=([^\s\"']+)", (o.read() + e.read()).decode()).group(1) +c.close() + +for domain in ["justtworoommates.com", "thepinkpulse.com", "freerangefruit.com"]: + req = urllib.request.Request( + f"{CF}/zones?name={domain}", + headers={"Authorization": f"Bearer {TOK}", "Content-Type": "application/json"}, + ) + with urllib.request.urlopen(req, timeout=30) as r: + res = json.load(r) + z = res["result"][0] + print(f"{domain}: {z.get('name_servers')}") \ No newline at end of file diff --git a/deploy/cf-probe-old-token.py b/deploy/cf-probe-old-token.py new file mode 100644 index 0000000..a612746 --- /dev/null +++ b/deploy/cf-probe-old-token.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 +import json +import urllib.error +import urllib.request + +CF = "https://api.cloudflare.com/client/v4" +ACCOUNT = "2599c23bbb1255dbb73e8d34b4115fda" +DOMAIN = "exposedgays.com" +TOK = "cfut_vF51avCWJV9EnCM4lXS2v60jXo6TPQZ0yxESHuRn5b0f3515" + + +def call(path, method="GET", body=None): + req = urllib.request.Request( + CF + path, + data=json.dumps(body).encode() if body else None, + method=method, + headers={"Authorization": f"Bearer {TOK}", "Content-Type": "application/json"}, + ) + try: + with urllib.request.urlopen(req, timeout=45) as r: + return json.load(r) + except urllib.error.HTTPError as e: + return json.loads(e.read().decode()) + + +for path in [ + "/user/tokens/verify", + f"/zones?name={DOMAIN}", + "/zones?per_page=50", + f"/accounts/{ACCOUNT}/tokens/verify", +]: + res = call(path) + print(path, "->", json.dumps(res)[:500]) + +create = call( + "/zones", + "POST", + {"name": DOMAIN, "account": {"id": ACCOUNT}, "type": "full", "jump_start": False}, +) +print("create ->", json.dumps(create)) \ No newline at end of file diff --git a/deploy/cf-probe.py b/deploy/cf-probe.py new file mode 100644 index 0000000..f2337d9 --- /dev/null +++ b/deploy/cf-probe.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 +import json, re, urllib.request, urllib.error, paramiko + +PASS = "Bbt9115xty9176!" +CF = "https://api.cloudflare.com/client/v4" + +def get_pfsense_token(): + c = paramiko.SSHClient() + c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + c.connect("10.10.0.1", username="admin", password=PASS, timeout=15) + _, o, e = c.exec_command("cat /usr/local/etc/wan-dns-failover.env", timeout=30) + o.channel.recv_exit_status() + text = (o.read() + e.read()).decode() + c.close() + return re.search(r"CLOUDFLARE_API_TOKEN=([^\s\"']+)", text).group(1) + +def call(token, path, method="GET", body=None): + req = urllib.request.Request( + CF + path, + data=json.dumps(body).encode() if body else None, + method=method, + headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"}, + ) + try: + with urllib.request.urlopen(req, timeout=30) as r: + return json.load(r) + except urllib.error.HTTPError as e: + return json.loads(e.read().decode()) + +tokens = { + "pfsense": get_pfsense_token(), + "apply_mail": "cfut_zlUt5lAKVsu7qIcRCHJnyhAvpJVF5jx24HlrIEn9a8fcd6a1", +} + +for name, tok in tokens.items(): + v = call(tok, "/user/tokens/verify") + print(name, "verify:", v.get("success"), v.get("messages"), v.get("errors")) + z = call(tok, "/zones?name=exposedgays.com") + print(name, "zone:", z.get("success"), len(z.get("result") or []), z.get("errors")) + c = call(tok, "/zones", "POST", {"name": "exposedgays.com", "account": {"id": "2599c23bbb1255dbb73e8d34b4115fda"}, "type": "full"}) + print(name, "create:", c.get("success"), c.get("errors")) + if c.get("success"): + print(" nameservers:", c["result"].get("name_servers")) \ No newline at end of file diff --git a/deploy/cf-server-token-scan.py b/deploy/cf-server-token-scan.py new file mode 100644 index 0000000..ec8725b --- /dev/null +++ b/deploy/cf-server-token-scan.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 +"""Scan COOLIFY_01 for all cfut_ tokens and try zone create for exposedgays.com.""" +import json +import re +import urllib.error +import urllib.request +import paramiko + +PASS = "Bbt9115xty9176!" +CF = "https://api.cloudflare.com/client/v4" +ACCOUNT = "2599c23bbb1255dbb73e8d34b4115fda" +DOMAIN = "exposedgays.com" +TUNNEL_CNAME = "03079a26-f14c-4622-b463-ba54a24f7472.cfargotunnel.com" + +ssh = paramiko.SSHClient() +ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +ssh.connect("10.10.0.10", username="localadministrator", password=PASS, timeout=30) + +_, o, e = ssh.exec_command( + f"echo '{PASS}' | sudo -S bash -c \"grep -rho 'cfut_[A-Za-z0-9_-]\\{{20,\\}}' " + "/etc/cloudflared /etc/infra /root /home/localadministrator 2>/dev/null | sort -u\"", + timeout=180, +) +o.channel.recv_exit_status() +tokens = sorted(set((o.read() + e.read()).decode().strip().splitlines())) +print(f"Found {len(tokens)} tokens on server") +ssh.close() + +# Also include known tokens +tokens += [ + "cfut_zlUt5lAKVsu7qIcRCHJnyhAvpJVF5jx24HlrIEn9a8fcd6a1", + "cfut_IOm1DJW4pDkPCuQA6UoOjqO6Tcv2zU10y2PSw82m379f7db3f7", +] +tokens = sorted(set(tokens)) + + +def call(tok, path, method="GET", body=None): + req = urllib.request.Request( + CF + path, + data=json.dumps(body).encode() if body else None, + method=method, + headers={"Authorization": f"Bearer {tok}", "Content-Type": "application/json"}, + ) + try: + with urllib.request.urlopen(req, timeout=45) as r: + return json.load(r) + except urllib.error.HTTPError as e: + return json.loads(e.read().decode()) + + +winner = None +zone = None + +for tok in tokens: + v = call(tok, "/user/tokens/verify") + if not v.get("success"): + continue + expired = any(m.get("code") == 10001 for m in v.get("messages", [])) + label = tok[:16] + "..." + print(f"\n{label} verify OK expired={expired}") + + z = call(tok, f"/zones?name={DOMAIN}") + if z.get("result"): + winner, zone = tok, z["result"][0] + print(f" FOUND zone {zone['id']} status={zone['status']}") + break + + c = call( + tok, + "/zones", + "POST", + {"name": DOMAIN, "account": {"id": ACCOUNT}, "type": "full", "jump_start": False}, + ) + if c.get("success"): + winner, zone = tok, c["result"] + print(f" CREATED zone NS={zone.get('name_servers')}") + break + err = (c.get("errors") or [{}])[0].get("message", "") + print(f" create fail: {err[:100]}") + +if not zone: + print("\nNo token could find or create zone") + raise SystemExit(1) + +zid = zone["id"] +body_apex = {"type": "CNAME", "name": DOMAIN, "content": TUNNEL_CNAME, "ttl": 1, "proxied": True} +body_www = {"type": "CNAME", "name": f"www.{DOMAIN}", "content": TUNNEL_CNAME, "ttl": 1, "proxied": True} + +records = call(winner, f"/zones/{zid}/dns_records?per_page=100").get("result", []) +for name, body in [(DOMAIN, body_apex), (f"www.{DOMAIN}", body_www)]: + existing = next( + (r for r in records if r["name"] == name and r["type"] in ("A", "CNAME", "AAAA")), + None, + ) + if existing: + res = call(winner, f"/zones/{zid}/dns_records/{existing['id']}", "PATCH", body) + else: + res = call(winner, f"/zones/{zid}/dns_records", "POST", body) + print(f"DNS {name}: success={res.get('success')} errors={res.get('errors')}") + +print("\n=== Nameservers (update GoDaddy if status=pending) ===") +for ns in zone.get("name_servers", []): + print(f" {ns}") +print(f"Zone status: {zone.get('status')}") \ No newline at end of file diff --git a/deploy/cf-token-perms.py b/deploy/cf-token-perms.py new file mode 100644 index 0000000..9d74c83 --- /dev/null +++ b/deploy/cf-token-perms.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +import json +import re +import urllib.error +import urllib.request +import paramiko + +PASS = "Bbt9115xty9176!" +CF = "https://api.cloudflare.com/client/v4" +ACCOUNT = "2599c23bbb1255dbb73e8d34b4115fda" +DOMAIN = "exposedgays.com" + + +def get_tokens(): + tokens = [] + c = paramiko.SSHClient() + c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + c.connect("10.10.0.1", username="admin", password=PASS, timeout=15) + _, o, e = c.exec_command("cat /usr/local/etc/wan-dns-failover.env", timeout=30) + o.channel.recv_exit_status() + text = (o.read() + e.read()).decode() + c.close() + tokens.append(("pfsense", re.search(r"CLOUDFLARE_API_TOKEN=([^\s\"']+)", text).group(1))) + tokens.append(("apply_mail", "cfut_zlUt5lAKVsu7qIcRCHJnyhAvpJVF5jx24HlrIEn9a8fcd6a1")) + return tokens + + +def call(tok, path, method="GET", body=None): + req = urllib.request.Request( + CF + path, + data=json.dumps(body).encode() if body else None, + method=method, + headers={"Authorization": f"Bearer {tok}", "Content-Type": "application/json"}, + ) + try: + with urllib.request.urlopen(req, timeout=45) as r: + return json.load(r) + except urllib.error.HTTPError as e: + return json.loads(e.read().decode()) + + +def main(): + for label, tok in get_tokens(): + v = call(tok, "/user/tokens/verify") + print(f"\n[{label}] verify:", json.dumps(v, indent=2)[:1200]) + z = call(tok, f"/zones?name={DOMAIN}") + print(f"[{label}] zone lookup:", z.get("success"), len(z.get("result") or [])) + c = call( + tok, + "/zones", + "POST", + {"name": DOMAIN, "account": {"id": ACCOUNT}, "type": "full", "jump_start": False}, + ) + print(f"[{label}] create:", c.get("success"), c.get("errors")) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/deploy/cf-try-all-tokens.py b/deploy/cf-try-all-tokens.py new file mode 100644 index 0000000..1bfc8d2 --- /dev/null +++ b/deploy/cf-try-all-tokens.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +import json +import re +import urllib.error +import urllib.request +import paramiko + +PASS = "Bbt9115xty9176!" +CF = "https://api.cloudflare.com/client/v4" +ACCOUNT = "2599c23bbb1255dbb73e8d34b4115fda" +DOMAIN = "exposedgays.com" +TUNNEL_CNAME = "03079a26-f14c-4622-b463-ba54a24f7472.cfargotunnel.com" + +TOKENS = [ + ("pfsense", None), + ("apply_mail", "cfut_zlUt5lAKVsu7qIcRCHJnyhAvpJVF5jx24HlrIEn9a8fcd6a1"), + ("local_old", "cfut_vF51avCWJV9EnCM4lXS2v60jXo6TPQZ0yxESHuRn5b0f3515"), + ("local_full", "cfut_IOm1DJW4pDkPCuQA6UoOjqO6Tcv2zU10y2PSw82m379f7db3f7"), + ("local_trunc", "cfut_IOm1DJW4pDkPCuQA6UoOjqO6Tcv2zU10y2PSw82m379f7db3"), +] + +try: + c = paramiko.SSHClient() + c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + c.connect("10.10.0.1", username="admin", password=PASS, timeout=15) + _, o, e = c.exec_command("cat /usr/local/etc/wan-dns-failover.env", timeout=30) + o.channel.recv_exit_status() + text = (o.read() + e.read()).decode() + c.close() + TOKENS[0] = ("pfsense", re.search(r"CLOUDFLARE_API_TOKEN=([^\s\"']+)", text).group(1)) +except Exception as ex: + print("pfsense err", ex) + + +def call(tok, path, method="GET", body=None): + req = urllib.request.Request( + CF + path, + data=json.dumps(body).encode() if body else None, + method=method, + headers={"Authorization": f"Bearer {tok}", "Content-Type": "application/json"}, + ) + try: + with urllib.request.urlopen(req, timeout=45) as r: + return json.load(r) + except urllib.error.HTTPError as e: + return json.loads(e.read().decode()) + + +def upsert_dns(tok, zid): + records = call(tok, f"/zones/{zid}/dns_records?per_page=100").get("result", []) + for name in [DOMAIN, f"www.{DOMAIN}"]: + existing = next( + (r for r in records if r["name"] == name and r["type"] in ("A", "CNAME", "AAAA")), + None, + ) + body = {"type": "CNAME", "name": name, "content": TUNNEL_CNAME, "ttl": 1, "proxied": True} + if existing: + res = call(tok, f"/zones/{zid}/dns_records/{existing['id']}", "PATCH", body) + action = "PATCH" + else: + res = call(tok, f"/zones/{zid}/dns_records", "POST", body) + action = "POST" + print(f" {action} {name}: success={res.get('success')} errors={res.get('errors')}") + + +def main(): + winner = None + zone = None + + for label, tok in TOKENS: + if not tok: + continue + v = call(tok, "/user/tokens/verify") + print(f"\n[{label}] verify={v.get('success')} errors={v.get('errors')}") + if not v.get("success"): + continue + + z = call(tok, f"/zones?name={DOMAIN}") + if z.get("result"): + winner, zone, tok_label = tok, z["result"][0], label + print(f"[{label}] FOUND existing zone {zone['id']} status={zone['status']}") + break + + c = call( + tok, + "/zones", + "POST", + {"name": DOMAIN, "account": {"id": ACCOUNT}, "type": "full", "jump_start": False}, + ) + print(f"[{label}] create={c.get('success')} errors={c.get('errors')}") + if c.get("success"): + winner, zone, tok_label = tok, c["result"], label + print(f"[{label}] CREATED zone NS={zone.get('name_servers')}") + break + + if not zone: + print("\nFAILED: no token could find or create zone") + return + + print(f"\nUsing token [{tok_label}] zone {zone['id']} status={zone['status']}") + print("Nameservers:") + for ns in zone.get("name_servers", []): + print(f" {ns}") + + upsert_dns(winner, zone["id"]) + + print("\nFinal records:") + for r in call(winner, f"/zones/{zone['id']}/dns_records?per_page=100").get("result", []): + if "exposedgays" in r["name"]: + print(f" {r['type']} {r['name']} -> {r['content']} proxied={r.get('proxied')}") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/deploy/cf-tunnel-api-probe.py b/deploy/cf-tunnel-api-probe.py new file mode 100644 index 0000000..0bf7bbb --- /dev/null +++ b/deploy/cf-tunnel-api-probe.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 +import json +import re +import urllib.error +import urllib.request +import paramiko + +PASS = "Bbt9115xty9176!" +CF = "https://api.cloudflare.com/client/v4" +ACCOUNT = "2599c23bbb1255dbb73e8d34b4115fda" +TUNNEL = "03079a26-f14c-4622-b463-ba54a24f7472" +DOMAIN = "exposedgays.com" + +c = paramiko.SSHClient() +c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +c.connect("10.10.0.1", username="admin", password=PASS, timeout=15) +_, o, e = c.exec_command("cat /usr/local/etc/wan-dns-failover.env", timeout=30) +o.channel.recv_exit_status() +TOK = re.search(r"CLOUDFLARE_API_TOKEN=([^\s\"']+)", (o.read() + e.read()).decode()).group(1) +c.close() + + +def call(path, method="GET", body=None): + req = urllib.request.Request( + CF + path, + data=json.dumps(body).encode() if body else None, + method=method, + headers={"Authorization": f"Bearer {TOK}", "Content-Type": "application/json"}, + ) + try: + with urllib.request.urlopen(req, timeout=45) as r: + return r.status, json.load(r) + except urllib.error.HTTPError as e: + return e.code, json.loads(e.read().decode()) + + +endpoints = [ + ("GET", f"/accounts/{ACCOUNT}/cfd_tunnel/{TUNNEL}/configurations"), + ("GET", f"/accounts/{ACCOUNT}/cfd_tunnel/{TUNNEL}"), + ("GET", f"/accounts/{ACCOUNT}/cfd_tunnel"), + ("POST", f"/accounts/{ACCOUNT}/cfd_tunnel/{TUNNEL}/routes", {"network": "0.0.0.0/0", "comment": "test"}), + ("POST", f"/accounts/{ACCOUNT}/cfd_tunnel/{TUNNEL}/connections", {}), +] +for method, path, *body in [(e[0], e[1], e[2] if len(e) > 2 else None) for e in endpoints]: + code, res = call(path, method, body[0] if body else None) + print(f"{method} {path}") + print(f" code={code} success={res.get('success')} errors={res.get('errors')}") \ No newline at end of file diff --git a/deploy/cf-tunnel-route-dns.py b/deploy/cf-tunnel-route-dns.py new file mode 100644 index 0000000..91d7c8b --- /dev/null +++ b/deploy/cf-tunnel-route-dns.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python3 +"""Route exposedgays.com DNS via cloudflared on COOLIFY_01.""" +import paramiko + +PASS = "Bbt9115xty9176!" +HOST = "10.10.0.10" +USER = "localadministrator" +TUNNEL = "03079a26-f14c-4622-b463-ba54a24f7472" +CREDS = f"/etc/cloudflared/{TUNNEL}.json" +DOMAIN = "exposedgays.com" + +c = paramiko.SSHClient() +c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +c.connect(HOST, username=USER, password=PASS, timeout=30) + +cmds = [ + f"echo '{PASS}' | sudo -S find / -name 'cert.pem' 2>/dev/null | head -20", + f"echo '{PASS}' | sudo -S cat {CREDS}", + f"echo '{PASS}' | sudo -S cloudflared tunnel --cred-file {CREDS} route dns -f {TUNNEL} {DOMAIN} 2>&1", + f"echo '{PASS}' | sudo -S cloudflared tunnel --cred-file {CREDS} route dns -f {TUNNEL} www.{DOMAIN} 2>&1", + "dig @1.1.1.1 exposedgays.com CNAME +short 2>&1", + "dig @1.1.1.1 www.exposedgays.com CNAME +short 2>&1", + "dig @1.1.1.1 exposedgays.com A +short 2>&1", +] +for cmd in cmds: + print("===", cmd[:100], "===") + _, o, e = c.exec_command(cmd, timeout=120) + o.channel.recv_exit_status() + out = (o.read() + e.read()).decode(errors="replace") + # redact tunnel secret if present + if "TunnelSecret" in out: + import re + out = re.sub(r'"TunnelSecret":\s*"[^"]+"', '"TunnelSecret": "[REDACTED]"', out) + print(out[:8000]) + print() + +c.close() \ No newline at end of file diff --git a/deploy/cf-zone-check.py b/deploy/cf-zone-check.py new file mode 100644 index 0000000..e777be4 --- /dev/null +++ b/deploy/cf-zone-check.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 +import json +import re +import urllib.request +import paramiko + +PASS = "Bbt9115xty9176!" +DOMAIN = "exposedgays.com" +CF = "https://api.cloudflare.com/client/v4" + +c = paramiko.SSHClient() +c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +c.connect("10.10.0.1", username="admin", password=PASS, timeout=15) +_, o, e = c.exec_command("cat /usr/local/etc/wan-dns-failover.env", timeout=30) +o.channel.recv_exit_status() +TOK = re.search(r"CLOUDFLARE_API_TOKEN=([^\s\"']+)", (o.read() + e.read()).decode()).group(1) +c.close() + +req = urllib.request.Request( + f"{CF}/zones?name={DOMAIN}", + headers={"Authorization": f"Bearer {TOK}", "Content-Type": "application/json"}, +) +with urllib.request.urlopen(req, timeout=30) as r: + res = json.load(r) + +print(json.dumps(res, indent=2)) \ No newline at end of file diff --git a/deploy/cf-zone-probe.py b/deploy/cf-zone-probe.py new file mode 100644 index 0000000..d84b4e6 --- /dev/null +++ b/deploy/cf-zone-probe.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 +import json +import re +import urllib.error +import urllib.request +import paramiko + +PASS = "Bbt9115xty9176!" +CF = "https://api.cloudflare.com/client/v4" +ACCOUNT = "2599c23bbb1255dbb73e8d34b4115fda" +DOMAIN = "exposedgays.com" + + +def get_tokens(): + tokens = [] + try: + c = paramiko.SSHClient() + c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + c.connect("10.10.0.1", username="admin", password=PASS, timeout=15) + _, o, e = c.exec_command("cat /usr/local/etc/wan-dns-failover.env", timeout=30) + o.channel.recv_exit_status() + text = (o.read() + e.read()).decode() + c.close() + m = re.search(r"CLOUDFLARE_API_TOKEN=([^\s\"']+)", text) + if m: + tokens.append(("pfsense", m.group(1))) + except Exception as ex: + print("pfsense err", ex) + + try: + c = paramiko.SSHClient() + c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + c.connect("10.10.0.10", username="localadministrator", password=PASS, timeout=30) + for path in [ + "/home/localadministrator/coder/onsethost/.env.local", + "/etc/infra/secrets.env", + "/root/.env", + ]: + _, o, e = c.exec_command( + f"grep CLOUDFLARE_API_TOKEN {path} 2>/dev/null | head -1", timeout=20 + ) + o.channel.recv_exit_status() + line = (o.read() + e.read()).decode().strip() + if line and "=" in line: + tokens.append((path, line.split("=", 1)[1].strip().strip('"'))) + c.close() + except Exception as ex: + print("coolify err", ex) + + tokens += [ + ("apply_mail", "cfut_zlUt5lAKVsu7qIcRCHJnyhAvpJVF5jx24HlrIEn9a8fcd6a1"), + ("onsethost", "cfut_IOm1DJW4pDkPCuQA6UoOjqO6Tcv2zU10y2PSw82m379f7db3"), + ] + return tokens + + +def call(tok, path, method="GET", body=None): + req = urllib.request.Request( + CF + path, + data=json.dumps(body).encode() if body else None, + method=method, + headers={"Authorization": f"Bearer {tok}", "Content-Type": "application/json"}, + ) + try: + with urllib.request.urlopen(req, timeout=45) as r: + return json.load(r) + except urllib.error.HTTPError as e: + return json.loads(e.read().decode()) + + +def main(): + for label, tok in get_tokens(): + v = call(tok, "/user/tokens/verify") + print(f"[{label}] verify:", v.get("success"), v.get("errors")) + z = call(tok, f"/zones?name={DOMAIN}") + print(f"[{label}] zone:", z.get("success"), len(z.get("result") or []), z.get("errors")) + c = call( + tok, + "/zones", + "POST", + {"name": DOMAIN, "account": {"id": ACCOUNT}, "type": "full", "jump_start": False}, + ) + print(f"[{label}] create:", c.get("success"), c.get("errors")) + if c.get("success"): + print(" NS:", c["result"].get("name_servers")) + print(" status:", c["result"].get("status")) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/deploy/check-avbeat-dns.py b/deploy/check-avbeat-dns.py new file mode 100644 index 0000000..ce6befc --- /dev/null +++ b/deploy/check-avbeat-dns.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python3 +import paramiko +PASS = "Bbt9115xty9176!" +c = paramiko.SSHClient() +c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +c.connect("10.10.0.10", username="localadministrator", password=PASS, timeout=30) +cmds = [ + "dig @1.1.1.1 avbeat.com NS +short", + "dig @1.1.1.1 avbeat.com A +short", + "dig @1.1.1.1 avbeat.com CNAME +short", + "dig @1.1.1.1 www.avbeat.com CNAME +short", + "curl -sSI --max-time 10 https://avbeat.com/ | head -6", +] +for cmd in cmds: + print(f"\n=== {cmd} ===") + _, o, e = c.exec_command(cmd, timeout=20) + o.channel.recv_exit_status() + print((o.read() + e.read()).decode()) +c.close() \ No newline at end of file diff --git a/deploy/check-cf-zone-dns.py b/deploy/check-cf-zone-dns.py new file mode 100644 index 0000000..c9ad975 --- /dev/null +++ b/deploy/check-cf-zone-dns.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 +import json +import re +import urllib.request +import paramiko + +PASS = "Bbt9115xty9176!" +DOMAIN = "exposedgays.com" +ZID = "b9e032992b315331c8b0571473fedc89" +CF = "https://api.cloudflare.com/client/v4" + +c = paramiko.SSHClient() +c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +c.connect("10.10.0.1", username="admin", password=PASS, timeout=15) +_, o, e = c.exec_command("cat /usr/local/etc/wan-dns-failover.env", timeout=30) +o.channel.recv_exit_status() +TOK = re.search(r"CLOUDFLARE_API_TOKEN=([^\s\"']+)", (o.read() + e.read()).decode()).group(1) +c.close() + +def cf(path): + req = urllib.request.Request( + CF + path, + headers={"Authorization": f"Bearer {TOK}", "Content-Type": "application/json"}, + ) + with urllib.request.urlopen(req, timeout=30) as r: + return json.load(r) + +zone = cf(f"/zones/{ZID}")["result"] +print("Zone status:", zone["status"]) +print("Nameservers:", zone["name_servers"]) +print("\nDNS records:") +for r in cf(f"/zones/{ZID}/dns_records?per_page=50")["result"]: + if DOMAIN in r["name"]: + print(f" {r['type']:6} {r['name']:25} -> {r['content'][:60]} proxied={r.get('proxied')}") \ No newline at end of file diff --git a/deploy/check-dns-full.py b/deploy/check-dns-full.py new file mode 100644 index 0000000..cb29976 --- /dev/null +++ b/deploy/check-dns-full.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +import paramiko +import subprocess + +PASS = "Bbt9115xty9176!" + +print("=== Local dig ===") +for q in ["exposedgays.com", "www.exposedgays.com"]: + r = subprocess.run(["nslookup", q, "1.1.1.1"], capture_output=True, text=True, timeout=20) + print(f"\n{q}:") + print(r.stdout or r.stderr) + +c = paramiko.SSHClient() +c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +c.connect("10.10.0.10", username="localadministrator", password=PASS, timeout=20) + +cmds = [ + "dig @1.1.1.1 exposedgays.com NS +short", + "dig @1.1.1.1 exposedgays.com A +short", + "dig @1.1.1.1 exposedgays.com CNAME +short", + "dig @1.1.1.1 www.exposedgays.com A +short", + "dig @1.1.1.1 www.exposedgays.com CNAME +short", + "curl -sSI https://exposedgays.com/ | head -15", + "curl -sSL https://exposedgays.com/ | head -c 400", + "curl -sSI -H 'Host: exposedgays.com' https://127.0.0.1/ -k | head -10", +] +for cmd in cmds: + print(f"\n=== {cmd} ===") + _, o, e = c.exec_command(cmd, timeout=30) + o.channel.recv_exit_status() + print((o.read() + e.read()).decode(errors="replace")) + +c.close() \ No newline at end of file diff --git a/deploy/check-dns-public.py b/deploy/check-dns-public.py new file mode 100644 index 0000000..a08ce29 --- /dev/null +++ b/deploy/check-dns-public.py @@ -0,0 +1,12 @@ +#!/usr/bin/env python3 +import urllib.request +import ssl + +ctx = ssl.create_default_context() +req = urllib.request.Request("https://exposedgays.com/", headers={"User-Agent": "Mozilla/5.0"}) +with urllib.request.urlopen(req, timeout=15, context=ctx) as r: + body = r.read(2000).decode(errors="replace") + print("status:", r.status) + print("headers:", dict(r.headers)) + print("body[:1500]:") + print(body[:1500]) \ No newline at end of file diff --git a/deploy/check-dns.py b/deploy/check-dns.py new file mode 100644 index 0000000..95694ab --- /dev/null +++ b/deploy/check-dns.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python3 +import paramiko + +c = paramiko.SSHClient() +c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +c.connect("10.10.0.10", username="localadministrator", password="Bbt9115xty9176!", timeout=20) + +cmds = [ + "grep -A3 'exposedgays' /etc/cloudflared/config.yml", + "curl -sS -o /dev/null -w 'local_traefik:%{http_code}' -k -H 'Host: exposedgays.com' https://127.0.0.1/", + "curl -sS -o /dev/null -w 'public:%{http_code}' --max-time 15 https://exposedgays.com/ 2>&1 || echo public_failed", +] + +for cmd in cmds: + print("$", cmd) + _, o, e = c.exec_command(cmd, timeout=30) + print((o.read() + e.read()).decode().strip()) + print() + +c.close() \ No newline at end of file diff --git a/deploy/check-tunnel-ingress-remote.py b/deploy/check-tunnel-ingress-remote.py new file mode 100644 index 0000000..f9c31a3 --- /dev/null +++ b/deploy/check-tunnel-ingress-remote.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 +import paramiko +PASS = "Bbt9115xty9176!" +c = paramiko.SSHClient() +c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +c.connect("10.10.0.10", username="localadministrator", password=PASS, timeout=30) +cmds = [ + "sudo journalctl -u cloudflared --no-pager -n 5 | grep -o 'hostname.*exposedgays' || echo 'not in recent log'", + "sudo journalctl -u cloudflared --no-pager -n 3 | grep 'Updated to new configuration' | tail -1 | grep -o 'exposedgays' || echo 'exposedgays not in remote config'", + "dig @annabel.ns.cloudflare.com exposedgays.com A +short", + "dig @annabel.ns.cloudflare.com www.exposedgays.com A +short", + "dig @1.1.1.1 exposedgays.com A +short", + "dig @1.1.1.1 www.exposedgays.com A +short", + "curl -sSI --max-time 20 https://exposedgays.com/ 2>&1 | head -12", + "curl -sSL --max-time 20 https://exposedgays.com/ 2>&1 | head -c 300", +] +for cmd in cmds: + print(f"\n=== {cmd} ===") + _, o, e = c.exec_command(cmd, timeout=40) + o.channel.recv_exit_status() + print((o.read() + e.read()).decode(errors="replace")[:2500]) +c.close() \ No newline at end of file diff --git a/deploy/check-tunnel-routes.py b/deploy/check-tunnel-routes.py new file mode 100644 index 0000000..d4458e7 --- /dev/null +++ b/deploy/check-tunnel-routes.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +import json, re, urllib.request, urllib.error, paramiko +PASS = "Bbt9115xty9176!" +ACCOUNT = "2599c23bbb1255dbb73e8d34b4115fda" +TUNNEL = "03079a26-f14c-4622-b463-ba54a24f7472" +ZID = "b9e032992b315331c8b0571473fedc89" +CF = "https://api.cloudflare.com/client/v4" + +c = paramiko.SSHClient(); c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +c.connect("10.10.0.1", username="admin", password=PASS, timeout=15) +_, o, e = c.exec_command("cat /usr/local/etc/wan-dns-failover.env", timeout=30) +o.channel.recv_exit_status() +TOK = re.search(r"CLOUDFLARE_API_TOKEN=([^\s\"']+)", (o.read()+e.read()).decode()).group(1) +c.close() + +def call(path): + req = urllib.request.Request(CF+path, headers={"Authorization": f"Bearer {TOK}", "Content-Type": "application/json"}) + try: + with urllib.request.urlopen(req, timeout=30) as r: return json.load(r) + except urllib.error.HTTPError as e: return json.loads(e.read().decode()) + +for path in [ + f"/accounts/{ACCOUNT}/cfd_tunnel/{TUNNEL}/routes?per_page=100", + f"/accounts/{ACCOUNT}/cfd_tunnel/{TUNNEL}", + f"/zones/{ZID}/dns_records?per_page=50", +]: + res = call(path) + print(f"\n{path}") + print("success=", res.get("success"), "errors=", res.get("errors")) + if "dns_records" in path or "routes" in path: + for item in res.get("result", []): + if "exposedgays" in str(item): + print(" ", json.dumps(item)[:300]) \ No newline at end of file diff --git a/deploy/compare-working-site.py b/deploy/compare-working-site.py new file mode 100644 index 0000000..c73850c --- /dev/null +++ b/deploy/compare-working-site.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python3 +import paramiko +PASS = "Bbt9115xty9176!" +c = paramiko.SSHClient() +c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +c.connect("10.10.0.10", username="localadministrator", password=PASS, timeout=30) +for domain in ["thepinkpulse.com", "freerangefruit.com", "justtworoommates.com"]: + cmds = [ + f"dig @1.1.1.1 {domain} NS +short", + f"dig @1.1.1.1 {domain} A +short", + f"dig @1.1.1.1 www.{domain} CNAME +short", + ] + print(f"\n--- {domain} ---") + for cmd in cmds: + _, o, e = c.exec_command(cmd, timeout=15) + o.channel.recv_exit_status() + out = (o.read() + e.read()).decode().strip() + print(cmd.split()[-2], "->", out or "(empty)") +c.close() \ No newline at end of file diff --git a/deploy/complete-dns.py b/deploy/complete-dns.py new file mode 100644 index 0000000..c6a80a6 --- /dev/null +++ b/deploy/complete-dns.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python3 +"""Complete exposedgays.com DNS: CF zone + tunnel CNAME + GoDaddy NS delegation.""" +import json +import re +import time +import urllib.error +import urllib.request +import paramiko + +PASS = "Bbt9115xty9176!" +DOMAIN = "exposedgays.com" +ACCOUNT = "2599c23bbb1255dbb73e8d34b4115fda" +TUNNEL_CNAME = "03079a26-f14c-4622-b463-ba54a24f7472.cfargotunnel.com" +CF = "https://api.cloudflare.com/client/v4" +GD = "https://api.godaddy.com/v1" +GODADDY_KEY = "2wYVpc3VT4_Gi9AsNqzJ6BtSfNz6nLDuo" +GODADDY_SECRET = "NvC3qexGAU7Rd5QExUTH6z" + + +def load_cf_token(): + c = paramiko.SSHClient() + c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + c.connect("10.10.0.1", username="admin", password=PASS, timeout=15) + _, o, e = c.exec_command("cat /usr/local/etc/wan-dns-failover.env", timeout=30) + o.channel.recv_exit_status() + text = (o.read() + e.read()).decode() + c.close() + return re.search(r"CLOUDFLARE_API_TOKEN=([^\s\"']+)", text).group(1) + + +def cf(tok, path, method="GET", body=None): + req = urllib.request.Request( + CF + path, + data=json.dumps(body).encode() if body is not None else None, + method=method, + headers={"Authorization": f"Bearer {tok}", "Content-Type": "application/json"}, + ) + try: + with urllib.request.urlopen(req, timeout=45) as r: + return json.load(r) + except urllib.error.HTTPError as e: + return json.loads(e.read().decode()) + + +def gd(path, method="GET", body=None): + req = urllib.request.Request( + GD + path, + data=json.dumps(body).encode() if body is not None else None, + method=method, + headers={ + "Authorization": f"sso-key {GODADDY_KEY}:{GODADDY_SECRET}", + "Content-Type": "application/json", + "Accept": "application/json", + }, + ) + try: + with urllib.request.urlopen(req, timeout=45) as r: + raw = r.read().decode() + return r.status, json.loads(raw) if raw else {} + except urllib.error.HTTPError as e: + raw = e.read().decode() + try: + return e.code, json.loads(raw) + except Exception: + return e.code, {"raw": raw[:500]} + + +def upsert_tunnel_cnames(tok, zid): + records = cf(tok, f"/zones/{zid}/dns_records?per_page=100").get("result", []) + for name in [DOMAIN, f"www.{DOMAIN}"]: + body = {"type": "CNAME", "name": name, "content": TUNNEL_CNAME, "ttl": 1, "proxied": True} + existing = next( + (r for r in records if r["name"] == name and r["type"] in ("A", "CNAME", "AAAA")), + None, + ) + if existing: + res = cf(tok, f"/zones/{zid}/dns_records/{existing['id']}", "PATCH", body) + action = "PATCH" + else: + res = cf(tok, f"/zones/{zid}/dns_records", "POST", body) + action = "POST" + print(f" {action} {name}: success={res.get('success')} errors={res.get('errors')}") + + +def main(): + tok = load_cf_token() + + zone = None + z = cf(tok, f"/zones?name={DOMAIN}") + if z.get("result"): + zone = z["result"][0] + print(f"Found zone {zone['id']} status={zone['status']}") + else: + created = cf( + tok, + "/zones", + "POST", + {"name": DOMAIN, "account": {"id": ACCOUNT}, "type": "full", "jump_start": False}, + ) + print("Create zone:", created.get("success"), created.get("errors")) + if created.get("success"): + zone = created["result"] + + if not zone: + print("BLOCKED: exposedgays.com not in Cloudflare and token lacks zone.create") + print("Add zone at https://dash.cloudflare.com then re-run this script") + return 1 + + zid = zone["id"] + nameservers = zone.get("name_servers", []) + print("Nameservers:", nameservers) + + print("Setting Cloudflare tunnel CNAME records...") + upsert_tunnel_cnames(tok, zid) + + if nameservers: + print("Updating GoDaddy nameservers...") + code, res = gd( + f"/domains/{DOMAIN}", + "PATCH", + {"nameServers": nameservers}, + ) + print(f"GoDaddy NS PATCH HTTP {code}: {res}") + + time.sleep(10) + c = paramiko.SSHClient() + c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + c.connect("10.10.0.10", username="localadministrator", password=PASS, timeout=30) + for cmd in [ + f"dig @1.1.1.1 {DOMAIN} NS +short", + f"dig @1.1.1.1 www.{DOMAIN} A +short", + f"curl -sSI https://www.{DOMAIN}/ | head -8", + f"curl -sSL https://www.{DOMAIN}/ | head -c 250", + ]: + print(f"\n$ {cmd}") + _, o, e = c.exec_command(cmd, timeout=30) + o.channel.recv_exit_status() + print((o.read() + e.read()).decode(errors="replace")) + c.close() + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) \ No newline at end of file diff --git a/deploy/deep-token-hunt.py b/deploy/deep-token-hunt.py new file mode 100644 index 0000000..ed1b5ff --- /dev/null +++ b/deploy/deep-token-hunt.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +import json +import urllib.error +import urllib.request +import paramiko + +PASS = "Bbt9115xty9176!" +ACCOUNT = "2599c23bbb1255dbb73e8d34b4115fda" +TUNNEL = "03079a26-f14c-4622-b463-ba54a24f7472" +CF = "https://api.cloudflare.com/client/v4" + +c = paramiko.SSHClient() +c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +c.connect("10.10.0.10", username="localadministrator", password=PASS, timeout=30) +_, o, e = c.exec_command( + f"echo '{PASS}' | sudo -S bash -c \"grep -rniE 'cfut_|GLOBAL_API_KEY|CLOUDFLARE_EMAIL|tunnel.*token|TUNNEL_TOKEN' " + "/etc/infra /etc/cloudflared /root /home/localadministrator/coder /home/localadministrator/scripts " + "2>/dev/null | grep -v node_modules | grep -v Binary | head -60\"", + timeout=120, +) +o.channel.recv_exit_status() +lines = (o.read() + e.read()).decode(errors="replace") +print(lines) +c.close() + +import re +tokens = sorted(set(re.findall(r"cfut_[A-Za-z0-9_-]{20,}", lines))) +tokens.append("cfut_zlUt5lAKVsu7qIcRCHJnyhAvpJVF5jx24HlrIEn9a8fcd6a1") +tokens = sorted(set(tokens)) +print(f"\nProbing {len(tokens)} tokens for tunnel PUT...") + +EG_RULES = [ + {"hostname": "exposedgays.com", "service": "https://localhost:443", "originRequest": {"noTLSVerify": True}}, + {"hostname": "www.exposedgays.com", "service": "https://localhost:443", "originRequest": {"noTLSVerify": True}}, +] + +for tok in tokens: + req = urllib.request.Request( + f"{CF}/accounts/{ACCOUNT}/cfd_tunnel/{TUNNEL}/configurations", + headers={"Authorization": f"Bearer {tok}", "Content-Type": "application/json"}, + ) + try: + with urllib.request.urlopen(req, timeout=30) as r: + data = json.load(r) + ingress = data["result"]["config"]["ingress"] + hostnames = {i.get("hostname") for i in ingress if i.get("hostname")} + changed = False + for rule in EG_RULES: + if rule["hostname"] not in hostnames: + catch = next((i for i, x in enumerate(ingress) if not x.get("hostname")), len(ingress)) + ingress.insert(catch, rule) + changed = True + if changed: + put_req = urllib.request.Request( + f"{CF}/accounts/{ACCOUNT}/cfd_tunnel/{TUNNEL}/configurations", + data=json.dumps({"config": {"ingress": ingress, "warp-routing": {"enabled": False}}}).encode(), + method="PUT", + headers={"Authorization": f"Bearer {tok}", "Content-Type": "application/json"}, + ) + with urllib.request.urlopen(put_req, timeout=60) as pr: + put = json.load(pr) + print(f"SUCCESS with {tok[:18]}... put={put.get('success')}") + raise SystemExit(0) + else: + print(f"{tok[:18]}... GET OK, already has exposedgays") + raise SystemExit(0) + except urllib.error.HTTPError as e: + err = json.loads(e.read().decode()) + print(f"{tok[:18]}... {e.code} {(err.get('errors') or [{}])[0].get('message','')[:50]}") + except SystemExit: + raise + except Exception as ex: + print(f"{tok[:18]}... err {ex}") + +print("No token could update tunnel config") \ No newline at end of file diff --git a/deploy/delegate-godaddy-to-cf.py b/deploy/delegate-godaddy-to-cf.py new file mode 100644 index 0000000..d29cf99 --- /dev/null +++ b/deploy/delegate-godaddy-to-cf.py @@ -0,0 +1,178 @@ +#!/usr/bin/env python3 +""" +Delegate exposedgays.com from GoDaddy to Cloudflare. +Requires exposedgays.com zone to exist in Cloudflare (dashboard add if API create fails). +""" +import json +import re +import sys +import time +import urllib.error +import urllib.request +import paramiko + +PASS = "Bbt9115xty9176!" +DOMAIN = "exposedgays.com" +ACCOUNT = "2599c23bbb1255dbb73e8d34b4115fda" +TUNNEL_CNAME = "03079a26-f14c-4622-b463-ba54a24f7472.cfargotunnel.com" +CF = "https://api.cloudflare.com/client/v4" +GD = "https://api.godaddy.com/v1" +GODADDY_KEY = "2wYVpc3VT4_Gi9AsNqzJ6BtSfNz6nLDuo" +GODADDY_SECRET = "NvC3qexGAU7Rd5QExUTH6z" + + +def load_cf_token(): + c = paramiko.SSHClient() + c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + c.connect("10.10.0.1", username="admin", password=PASS, timeout=15) + _, o, e = c.exec_command("cat /usr/local/etc/wan-dns-failover.env", timeout=30) + o.channel.recv_exit_status() + text = (o.read() + e.read()).decode() + c.close() + return re.search(r"CLOUDFLARE_API_TOKEN=([^\s\"']+)", text).group(1) + + +def cf(tok, path, method="GET", body=None): + req = urllib.request.Request( + CF + path, + data=json.dumps(body).encode() if body is not None else None, + method=method, + headers={"Authorization": f"Bearer {tok}", "Content-Type": "application/json"}, + ) + try: + with urllib.request.urlopen(req, timeout=45) as r: + return json.load(r) + except urllib.error.HTTPError as e: + return json.loads(e.read().decode()) + + +def gd(path, method="GET", body=None): + req = urllib.request.Request( + GD + path, + data=json.dumps(body).encode() if body is not None else None, + method=method, + headers={ + "Authorization": f"sso-key {GODADDY_KEY}:{GODADDY_SECRET}", + "Content-Type": "application/json", + "Accept": "application/json", + }, + ) + try: + with urllib.request.urlopen(req, timeout=45) as r: + raw = r.read().decode() + return r.status, json.loads(raw) if raw else {} + except urllib.error.HTTPError as e: + raw = e.read().decode() + try: + return e.code, json.loads(raw) + except Exception: + return e.code, {"raw": raw[:800]} + + +def find_zone(tok): + for status in ["active", "pending", ""]: + q = f"name={DOMAIN}" + (f"&status={status}" if status else "") + res = cf(tok, f"/zones?{q}") + if res.get("result"): + return res["result"][0] + page = 1 + while page <= 20: + res = cf(tok, f"/zones?per_page=50&page={page}") + for z in res.get("result", []): + if z["name"] == DOMAIN: + return z + if len(res.get("result", [])) < 50: + break + page += 1 + return None + + +def upsert_tunnel_cnames(tok, zid): + records = cf(tok, f"/zones/{zid}/dns_records?per_page=100").get("result", []) + for name in [DOMAIN, f"www.{DOMAIN}"]: + body = {"type": "CNAME", "name": name, "content": TUNNEL_CNAME, "ttl": 1, "proxied": True} + existing = next( + (r for r in records if r["name"] == name and r["type"] in ("A", "CNAME", "AAAA")), + None, + ) + if existing: + res = cf(tok, f"/zones/{zid}/dns_records/{existing['id']}", "PATCH", body) + action = "PATCH" + else: + res = cf(tok, f"/zones/{zid}/dns_records", "POST", body) + action = "POST" + print(f" CF {action} {name}: success={res.get('success')} errors={res.get('errors')}") + + +def verify(): + c = paramiko.SSHClient() + c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + c.connect("10.10.0.10", username="localadministrator", password=PASS, timeout=30) + cmds = [ + f"dig @1.1.1.1 {DOMAIN} NS +short", + f"dig @1.1.1.1 www.{DOMAIN} A +short", + f"curl -sSI https://www.{DOMAIN}/ | head -10", + f"curl -sSL https://www.{DOMAIN}/ | head -c 200", + ] + for cmd in cmds: + print(f"\n$ {cmd}") + _, o, e = c.exec_command(cmd, timeout=30) + o.channel.recv_exit_status() + print((o.read() + e.read()).decode(errors="replace")) + c.close() + + +def main(): + tok = load_cf_token() + zone = find_zone(tok) + + if not zone: + created = cf( + tok, + "/zones", + "POST", + {"name": DOMAIN, "account": {"id": ACCOUNT}, "type": "full", "jump_start": False}, + ) + if created.get("success"): + zone = created["result"] + print("Created CF zone via API") + else: + print("CF zone not found and API create failed:") + print(" ", created.get("errors")) + print("\nAdd exposedgays.com at https://dash.cloudflare.com then re-run:") + print(" python exposedgays/deploy/delegate-godaddy-to-cf.py") + return 1 + + zid = zone["id"] + nameservers = zone.get("name_servers", []) + status = zone.get("status") + print(f"CF zone {zid} status={status}") + print(f"CF nameservers: {nameservers}") + + print("\n=== GoDaddy before ===") + code, before = gd(f"/domains/{DOMAIN}") + print(f"HTTP {code} NS={before.get('nameServers') if code == 200 else before}") + + print("\n=== Cloudflare tunnel DNS ===") + upsert_tunnel_cnames(tok, zid) + + if not nameservers: + print("No nameservers on zone — cannot delegate GoDaddy") + return 1 + + print("\n=== GoDaddy PATCH nameservers -> Cloudflare ===") + code, res = gd(f"/domains/{DOMAIN}", "PATCH", {"nameServers": nameservers}) + print(f"HTTP {code}: {json.dumps(res, indent=2) if res else '{}'}") + + code, after = gd(f"/domains/{DOMAIN}") + if code == 200: + print(f"\nGoDaddy after NS={after.get('nameServers')}") + + print("\nWaiting 15s for propagation...") + time.sleep(15) + verify() + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) \ No newline at end of file diff --git a/deploy/deploy-v2-features.py b/deploy/deploy-v2-features.py new file mode 100644 index 0000000..2ac34b0 --- /dev/null +++ b/deploy/deploy-v2-features.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 +"""Push v2 features and deploy ExposedGays on Coolify.""" +import paramiko +import subprocess +import os + +HOST = "10.10.0.10" +USER = "localadministrator" +PASS = "Bbt9115xty9176!" +GITEA_TOKEN = "5c323b8ea677d78b201341f6833d57156a607008" +APP = "ira4bw4nhbvm87o7s3fgqu6v" +REPO_DIR = r"C:\Users\Local Administrator\exposedgays" + +def local_git(*args): + cmd = ["git", "-C", REPO_DIR] + list(args) + print("$", " ".join(cmd)) + r = subprocess.run(cmd, capture_output=True, text=True) + print(r.stdout[-4000:] if r.stdout else "") + if r.stderr: + print(r.stderr[-2000:]) + return r.returncode + +# Stage and commit locally +local_git("add", "-A") +local_git("status", "--short") +local_git( + "commit", "-m", + "v2: ID verification by state, 15 photos/5 albums, 20 quick replies, logo, mobile polish" +) +local_git("push", "origin", "main") + +c = paramiko.SSHClient() +c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +c.connect(HOST, username=USER, password=PASS, timeout=20, allow_agent=False, look_for_keys=False) + +def run(cmd, t=300): + print("$", cmd[:180]) + _, o, e = c.exec_command(cmd, timeout=t) + code = o.channel.recv_exit_status() + out = (o.read() + e.read()).decode(errors="replace") + print(out[-6000:]) + print("exit", code, "\n") + return code, out + +run("cd /tmp/exposedgays && git pull origin main") +run( + "TOKEN=$(cat /home/localadministrator/.coolify-token | tail -1); " + f"curl -sS -X POST 'http://127.0.0.1:8000/api/v1/deploy?uuid={APP}&force_rebuild=true' " + '-H "Authorization: Bearer $TOKEN"' +) + +for i in range(120): + _, out = run("docker exec coolify php artisan queue:work --queue=high,default --once --timeout=900 2>&1", t=920) + if "ApplicationDeploymentJob" in out: + if "DONE" in out: + print("=== DEPLOY SUCCESS ===") + break + if "FAIL" in out: + print("=== DEPLOY FAILED ===") + break + +run("curl -sS -o /dev/null -w '%{http_code}' https://exposedgays.com/ 2>&1 || curl -sS -o /dev/null -w '%{http_code}' http://127.0.0.1:3000/ 2>&1") +c.close() +print("Done.") \ No newline at end of file diff --git a/deploy/diag-tunnel.py b/deploy/diag-tunnel.py new file mode 100644 index 0000000..d5c04d0 --- /dev/null +++ b/deploy/diag-tunnel.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 +import paramiko + +PASS = "Bbt9115xty9176!" +c = paramiko.SSHClient() +c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +c.connect("10.10.0.10", username="localadministrator", password=PASS, timeout=30) + +cmds = [ + "sudo systemctl is-active cloudflared", + "sudo systemctl status cloudflared --no-pager | head -20", + "sudo ls -la /etc/cloudflared/", + "sudo ls -la /root/.cloudflared/ 2>/dev/null || echo no_root_cloudflared", + "sudo ls -la /home/localadministrator/.cloudflared/ 2>/dev/null || echo no_home_cloudflared", + "dig @1.1.1.1 www.exposedgays.com A +short", + "dig @1.1.1.1 www.onsethost.com CNAME +short", + "dig @1.1.1.1 coolify.onsethost.com CNAME +short", + "curl -sSI --max-time 15 https://coolify.onsethost.com/ | head -8", + "curl -sSI --max-time 15 https://www.exposedgays.com/ 2>&1 | head -8", +] +for cmd in cmds: + print(f"\n=== {cmd} ===") + _, o, e = c.exec_command(cmd, timeout=40) + o.channel.recv_exit_status() + print((o.read() + e.read()).decode(errors="replace")[:3000]) +c.close() \ No newline at end of file diff --git a/deploy/dns-trace.py b/deploy/dns-trace.py new file mode 100644 index 0000000..9defa52 --- /dev/null +++ b/deploy/dns-trace.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python3 +import paramiko + +PASS = "Bbt9115xty9176!" +c = paramiko.SSHClient() +c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +c.connect("10.10.0.10", username="localadministrator", password=PASS, timeout=30) + +cmds = [ + "dig @ns71.domaincontrol.com www.exposedgays.com ANY +noall +answer", + "dig @ns71.domaincontrol.com exposedgays.com ANY +noall +answer", + "dig +trace www.exposedgays.com A 2>&1 | tail -20", + "dig 03079a26-f14c-4622-b463-ba54a24f7472.cfargotunnel.com A +short", + "getent hosts www.exposedgays.com", + "resolvectl query www.exposedgays.com 2>/dev/null || systemd-resolve --status www.exposedgays.com 2>/dev/null | head -20", +] +for cmd in cmds: + print(f"\n=== {cmd} ===") + _, o, e = c.exec_command(cmd, timeout=60) + o.channel.recv_exit_status() + print((o.read() + e.read()).decode(errors="replace")[:4000]) + +c.close() \ No newline at end of file diff --git a/deploy/final-verify-and-fix.py b/deploy/final-verify-and-fix.py new file mode 100644 index 0000000..5f0b573 --- /dev/null +++ b/deploy/final-verify-and-fix.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 +"""Verify DNS, attempt tunnel route via API endpoints we can access, restart cloudflared.""" +import json +import re +import time +import urllib.error +import urllib.request +import paramiko + +PASS = "Bbt9115xty9176!" +ACCOUNT = "2599c23bbb1255dbb73e8d34b4115fda" +TUNNEL = "03079a26-f14c-4622-b463-ba54a24f7472" +ZID = "b9e032992b315331c8b0571473fedc89" +CF = "https://api.cloudflare.com/client/v4" +HOSTS = ["exposedgays.com", "www.exposedgays.com"] + +c = paramiko.SSHClient() +c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +c.connect("10.10.0.1", username="admin", password=PASS, timeout=15) +_, o, e = c.exec_command("cat /usr/local/etc/wan-dns-failover.env", timeout=30) +o.channel.recv_exit_status() +TOK = re.search(r"CLOUDFLARE_API_TOKEN=([^\s\"']+)", (o.read() + e.read()).decode()).group(1) +c.close() + + +def cf(path, method="GET", body=None): + req = urllib.request.Request( + CF + path, + data=json.dumps(body).encode() if body else None, + method=method, + headers={"Authorization": f"Bearer {TOK}", "Content-Type": "application/json"}, + ) + try: + with urllib.request.urlopen(req, timeout=45) as r: + return r.status, json.load(r) + except urllib.error.HTTPError as e: + raw = e.read().decode() + try: + return e.code, json.loads(raw) + except Exception: + return e.code, {"raw": raw[:300]} + + +# Ensure DNS records are correct (re-upsert proxied CNAMEs) +print("=== CF DNS records ===") +code, recs = cf(f"/zones/{ZID}/dns_records?per_page=100") +records = recs.get("result", []) +for name in HOSTS: + body = { + "type": "CNAME", + "name": name, + "content": f"{TUNNEL}.cfargotunnel.com", + "ttl": 1, + "proxied": True, + } + existing = next((r for r in records if r["name"] == name and r["type"] in ("A", "CNAME")), None) + if existing: + code, res = cf(f"/zones/{ZID}/dns_records/{existing['id']}", "PATCH", body) + act = "PATCH" + else: + code, res = cf(f"/zones/{ZID}/dns_records", "POST", body) + act = "POST" + print(f" {act} {name}: HTTP {code} success={res.get('success')}") + +# Try teamnet hostname route endpoints (some accounts expose these) +for path, method, body in [ + (f"/accounts/{ACCOUNT}/teamnet/routes", "POST", {"network": "exposedgays.com", "tunnel_id": TUNNEL, "comment": "exposedgays apex"}), + (f"/accounts/{ACCOUNT}/teamnet/routes", "POST", {"network": "www.exposedgays.com", "tunnel_id": TUNNEL, "comment": "exposedgays www"}), +]: + code, res = cf(path, method, body) + print(f"\n{method} {path}: HTTP {code} success={res.get('success')} errors={res.get('errors')}") + +# Restart cloudflared and verify +ssh = paramiko.SSHClient() +ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +ssh.connect("10.10.0.10", username="localadministrator", password=PASS, timeout=30) +ssh.exec_command(f"echo '{PASS}' | sudo -S systemctl restart cloudflared") +time.sleep(12) + +cmds = [ + "dig @annabel.ns.cloudflare.com exposedgays.com A +short", + "dig @annabel.ns.cloudflare.com www.exposedgays.com A +short", + "sudo journalctl -u cloudflared --no-pager -n 4 | grep -o 'exposedgays' | head -3 || echo NO_EXPOSEDGAYS_IN_REMOTE", + "curl -sSI --max-time 25 https://exposedgays.com/ 2>&1 | head -15", + "curl -sSI --max-time 25 https://www.exposedgays.com/ 2>&1 | head -15", + "curl -sSL --max-time 25 https://www.exposedgays.com/ 2>&1 | head -c 400", +] +for cmd in cmds: + print(f"\n$ {cmd}") + _, o, e = ssh.exec_command(cmd, timeout=40) + o.channel.recv_exit_status() + print((o.read() + e.read()).decode(errors="replace")[:2500]) +ssh.close() \ No newline at end of file diff --git a/deploy/find-onsethost-cf-env.py b/deploy/find-onsethost-cf-env.py new file mode 100644 index 0000000..d99e08d --- /dev/null +++ b/deploy/find-onsethost-cf-env.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 +import paramiko + +PASS = "Bbt9115xty9176!" +c = paramiko.SSHClient() +c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +c.connect("10.10.0.10", username="localadministrator", password=PASS, timeout=30) + +cmds = [ + "docker ps --format '{{.Names}}' | grep -i onset | head -10", + "docker ps --format '{{.Names}}' | grep -i exposed | head -5", + f"echo '{PASS}' | sudo -S cat /etc/infra/secrets.env 2>/dev/null | grep -i cloudflare", + "grep -h CLOUDFLARE /home/localadministrator/coder/onsethost/.env.local /home/localadministrator/coder/onsethost/.env 2>/dev/null", +] +for cmd in cmds: + print("===", cmd, "===") + _, o, e = c.exec_command(cmd, timeout=60) + o.channel.recv_exit_status() + print((o.read() + e.read()).decode(errors="replace")[:5000]) + print() + +# Get onsethost container env +_, o, e = c.exec_command( + "docker ps --format '{{.Names}}' | grep -E 'onsethost|woeslw' | head -3", timeout=30 +) +o.channel.recv_exit_status() +names = (o.read() + e.read()).decode().strip().splitlines() +for name in names: + if not name.strip(): + continue + print(f"=== env {name} CLOUDFLARE ===") + _, o2, e2 = c.exec_command(f"docker exec {name.strip()} env 2>/dev/null | grep -i CLOUDFLARE", timeout=30) + o2.channel.recv_exit_status() + print((o2.read() + e2.read()).decode(errors="replace")) + +c.close() \ No newline at end of file diff --git a/deploy/finish-after-cf-zone.py b/deploy/finish-after-cf-zone.py new file mode 100644 index 0000000..98f3dc3 --- /dev/null +++ b/deploy/finish-after-cf-zone.py @@ -0,0 +1,179 @@ +#!/usr/bin/env python3 +""" +Run AFTER adding exposedgays.com in Cloudflare dashboard. +Completes: tunnel public hostnames (if token allows), CF DNS CNAMEs, GoDaddy NS delegation. +""" +import json +import re +import sys +import time +import urllib.error +import urllib.request +import paramiko + +PASS = "Bbt9115xty9176!" +DOMAIN = "exposedgays.com" +ACCOUNT = "2599c23bbb1255dbb73e8d34b4115fda" +TUNNEL_ID = "03079a26-f14c-4622-b463-ba54a24f7472" +TUNNEL_CNAME = f"{TUNNEL_ID}.cfargotunnel.com" +CF = "https://api.cloudflare.com/client/v4" +GD = "https://api.godaddy.com/v1" +GODADDY_KEY = "2wYVpc3VT4_Gi9AsNqzJ6BtSfNz6nLDuo" +GODADDY_SECRET = "NvC3qexGAU7Rd5QExUTH6z" +EG_RULES = [ + {"hostname": "exposedgays.com", "service": "https://localhost:443", "originRequest": {"noTLSVerify": True}}, + {"hostname": "www.exposedgays.com", "service": "https://localhost:443", "originRequest": {"noTLSVerify": True}}, +] + + +def load_cf_token(): + c = paramiko.SSHClient() + c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + c.connect("10.10.0.1", username="admin", password=PASS, timeout=15) + _, o, e = c.exec_command("cat /usr/local/etc/wan-dns-failover.env", timeout=30) + o.channel.recv_exit_status() + text = (o.read() + e.read()).decode() + c.close() + return re.search(r"CLOUDFLARE_API_TOKEN=([^\s\"']+)", text).group(1) + + +def cf(tok, path, method="GET", body=None): + req = urllib.request.Request( + CF + path, + data=json.dumps(body).encode() if body is not None else None, + method=method, + headers={"Authorization": f"Bearer {tok}", "Content-Type": "application/json"}, + ) + try: + with urllib.request.urlopen(req, timeout=60) as r: + return r.status, json.load(r) + except urllib.error.HTTPError as e: + return e.code, json.loads(e.read().decode()) + + +def gd(path, method="GET", body=None): + req = urllib.request.Request( + GD + path, + data=json.dumps(body).encode() if body is not None else None, + method=method, + headers={ + "Authorization": f"sso-key {GODADDY_KEY}:{GODADDY_SECRET}", + "Content-Type": "application/json", + "Accept": "application/json", + }, + ) + try: + with urllib.request.urlopen(req, timeout=45) as r: + raw = r.read().decode() + return r.status, json.loads(raw) if raw else {} + except urllib.error.HTTPError as e: + raw = e.read().decode() + try: + return e.code, json.loads(raw) + except Exception: + return e.code, {"raw": raw[:500]} + + +def upsert_tunnel_cnames(tok, zid): + records = cf(tok, f"/zones/{zid}/dns_records?per_page=100")[1].get("result", []) + for name in [DOMAIN, f"www.{DOMAIN}"]: + body = {"type": "CNAME", "name": name, "content": TUNNEL_CNAME, "ttl": 1, "proxied": True} + existing = next((r for r in records if r["name"] == name and r["type"] in ("A", "CNAME", "AAAA")), None) + if existing: + code, res = cf(tok, f"/zones/{zid}/dns_records/{existing['id']}", "PATCH", body) + action = "PATCH" + else: + code, res = cf(tok, f"/zones/{zid}/dns_records", "POST", body) + action = "POST" + print(f" {action} {name}: HTTP {code} success={res.get('success')} errors={res.get('errors')}") + + +def try_tunnel_ingress(tok): + code, body = cf(tok, f"/accounts/{ACCOUNT}/cfd_tunnel/{TUNNEL_ID}/configurations") + if code != 200 or not body.get("result"): + print("Tunnel config API blocked — add Public Hostnames manually in Zero Trust dashboard:") + print(" exposedgays.com -> https://localhost:443 (No TLS Verify)") + print(" www.exposedgays.com -> https://localhost:443 (No TLS Verify)") + return False + + ingress = body["result"].get("config", {}).get("ingress", []) + hostnames = {i.get("hostname") for i in ingress if i.get("hostname")} + changed = False + for rule in EG_RULES: + if rule["hostname"] not in hostnames: + catch = next((i for i, x in enumerate(ingress) if not x.get("hostname")), len(ingress)) + ingress.insert(catch, rule) + changed = True + print(" added tunnel ingress", rule["hostname"]) + + if not changed: + print(" tunnel ingress already has exposedgays hostnames") + return True + + put_code, put = cf( + tok, + f"/accounts/{ACCOUNT}/cfd_tunnel/{TUNNEL_ID}/configurations", + "PUT", + {"config": {"ingress": ingress, "warp-routing": {"enabled": False}}}, + ) + print(f" PUT tunnel config HTTP {put_code} success={put.get('success')} errors={put.get('errors')}") + if put.get("success"): + c = paramiko.SSHClient() + c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + c.connect("10.10.0.10", username="localadministrator", password=PASS, timeout=30) + c.exec_command(f"echo '{PASS}' | sudo -S systemctl restart cloudflared") + c.close() + time.sleep(8) + return put.get("success", False) + + +def main(): + tok = load_cf_token() + code, zres = cf(tok, f"/zones?name={DOMAIN}") + if not zres.get("result"): + print(f"BLOCKED: {DOMAIN} not in Cloudflare yet.") + print("Add it at https://dash.cloudflare.com → Add a Site → exposedgays.com (Free plan)") + print("Then re-run: python exposedgays/deploy/finish-after-cf-zone.py") + return 1 + + zone = zres["result"][0] + zid = zone["id"] + nameservers = zone.get("name_servers", []) + print(f"Found zone {zid} status={zone['status']}") + print(f"Nameservers: {nameservers}") + + print("\n=== Tunnel ingress ===") + try_tunnel_ingress(tok) + + print("\n=== Cloudflare DNS (proxied CNAME -> tunnel) ===") + upsert_tunnel_cnames(tok, zid) + + if nameservers: + print("\n=== GoDaddy NS -> Cloudflare ===") + gcode, gres = gd(f"/domains/{DOMAIN}", "PATCH", {"nameServers": nameservers}) + print(f"PATCH HTTP {gcode}: {gres}") + _, after = gd(f"/domains/{DOMAIN}") + print("GoDaddy NS now:", after.get("nameServers")) + + print("\nWaiting 20s...") + time.sleep(20) + + c = paramiko.SSHClient() + c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + c.connect("10.10.0.10", username="localadministrator", password=PASS, timeout=30) + for cmd in [ + f"dig @1.1.1.1 {DOMAIN} A +short", + f"dig @1.1.1.1 www.{DOMAIN} A +short", + f"curl -sSI --max-time 20 https://{DOMAIN}/ | head -10", + f"curl -sSL --max-time 20 https://{DOMAIN}/ | head -c 200", + ]: + print(f"\n$ {cmd}") + _, o, e = c.exec_command(cmd, timeout=30) + o.channel.recv_exit_status() + print((o.read() + e.read()).decode(errors="replace")) + c.close() + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) \ No newline at end of file diff --git a/deploy/fix-dns-now.py b/deploy/fix-dns-now.py new file mode 100644 index 0000000..7125a7b --- /dev/null +++ b/deploy/fix-dns-now.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python3 +"""Emergency DNS fix: revert GoDaddy NS + point records at tunnel.""" +import json +import time +import urllib.error +import urllib.request +import paramiko + +GODADDY_KEY = "2wYVpc3VT4_Gi9AsNqzJ6BtSfNz6nLDuo" +GODADDY_SECRET = "NvC3qexGAU7Rd5QExUTH6z" +DOMAIN = "exposedgays.com" +TUNNEL_CNAME = "03079a26-f14c-4622-b463-ba54a24f7472.cfargotunnel.com" +GD_NS = ["ns71.domaincontrol.com", "ns72.domaincontrol.com"] +GD = "https://api.godaddy.com/v1" +PASS = "Bbt9115xty9176!" + + +def gd(path, method="GET", body=None): + req = urllib.request.Request( + GD + path, + data=json.dumps(body).encode() if body is not None else None, + method=method, + headers={ + "Authorization": f"sso-key {GODADDY_KEY}:{GODADDY_SECRET}", + "Content-Type": "application/json", + "Accept": "application/json", + }, + ) + try: + with urllib.request.urlopen(req, timeout=45) as r: + raw = r.read().decode() + return r.status, json.loads(raw) if raw else {} + except urllib.error.HTTPError as e: + raw = e.read().decode() + try: + return e.code, json.loads(raw) + except Exception: + return e.code, {"raw": raw[:800]} + + +def main(): + code, before = gd(f"/domains/{DOMAIN}") + print("Before NS:", before.get("nameServers") if code == 200 else before) + + print("\n=== Revert registrar NS to GoDaddy ===") + code, res = gd(f"/domains/{DOMAIN}", "PATCH", {"nameServers": GD_NS}) + print(f"PATCH NS HTTP {code}: {res}") + + code, mid = gd(f"/domains/{DOMAIN}") + print("After NS revert:", mid.get("nameServers") if code == 200 else mid) + + print("\n=== Set tunnel CNAME for www ===") + code, res = gd( + f"/domains/{DOMAIN}/records/CNAME/www", + "PUT", + [{"data": TUNNEL_CNAME, "ttl": 600}], + ) + print(f"PUT www CNAME HTTP {code}: {res}") + + print("\n=== Try apex CNAME (GoDaddy may reject) ===") + code, res = gd( + f"/domains/{DOMAIN}/records/CNAME/@", + "PUT", + [{"data": TUNNEL_CNAME, "ttl": 600}], + ) + print(f"PUT @ CNAME HTTP {code}: {res}") + + if code not in (200, 204): + print("\n=== Apex CNAME failed — enable domain forward @ -> www ===") + code, res = gd( + f"/domains/{DOMAIN}/forwarding", + "PATCH", + { + "forwarding": { + "type": "PERMANENT", + "url": f"https://www.{DOMAIN}", + "mask": False, + } + }, + ) + print(f"PATCH forwarding HTTP {code}: {res}") + + print("\n=== Current GoDaddy records ===") + code, records = gd(f"/domains/{DOMAIN}/records") + for r in records if isinstance(records, list) else []: + print(f" {r['type']:5} {r['name']:20} -> {r.get('data')}") + + print("\nWaiting 20s for propagation...") + time.sleep(20) + + c = paramiko.SSHClient() + c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + c.connect("10.10.0.10", username="localadministrator", password=PASS, timeout=30) + for cmd in [ + f"dig @1.1.1.1 {DOMAIN} NS +short", + f"dig @1.1.1.1 {DOMAIN} A +short", + f"dig @1.1.1.1 www.{DOMAIN} CNAME +short", + f"curl -sSI https://www.{DOMAIN}/ | head -12", + f"curl -sSL https://www.{DOMAIN}/ | head -c 300", + f"curl -sSI https://{DOMAIN}/ | head -12", + ]: + print(f"\n$ {cmd}") + _, o, e = c.exec_command(cmd, timeout=30) + o.channel.recv_exit_status() + print((o.read() + e.read()).decode(errors="replace")) + c.close() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/deploy/fix-exposedgays-dns-direct.py b/deploy/fix-exposedgays-dns-direct.py new file mode 100644 index 0000000..0685c7a --- /dev/null +++ b/deploy/fix-exposedgays-dns-direct.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python3 +"""Fix exposedgays.com 404: route via direct A record like thepinkpulse.com.""" +import json +import re +import time +import urllib.error +import urllib.request +import paramiko + +PASS = "Bbt9115xty9176!" +ZID = "b9e032992b315331c8b0571473fedc89" +ORIGIN_IP = "68.248.192.234" +HOSTS = ["exposedgays.com", "www.exposedgays.com"] +CF = "https://api.cloudflare.com/client/v4" + + +def get_token(): + c = paramiko.SSHClient() + c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + c.connect("10.10.0.1", username="admin", password=PASS, timeout=15) + _, o, e = c.exec_command("cat /usr/local/etc/wan-dns-failover.env", timeout=30) + o.channel.recv_exit_status() + text = (o.read() + e.read()).decode() + c.close() + return re.search(r"CLOUDFLARE_API_TOKEN=([^\s\"']+)", text).group(1) + + +def cf(token, path, method="GET", body=None): + req = urllib.request.Request( + CF + path, + data=json.dumps(body).encode() if body else None, + method=method, + headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"}, + ) + try: + with urllib.request.urlopen(req, timeout=45) as r: + return r.status, json.load(r) + except urllib.error.HTTPError as e: + return e.code, json.loads(e.read().decode()) + + +def ssh_check(): + c = paramiko.SSHClient() + c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + c.connect("10.10.0.10", username="localadministrator", password=PASS, timeout=30) + cmds = [ + "curl -sSI --max-time 20 https://exposedgays.com/ | head -12", + "curl -sSI --max-time 20 https://www.exposedgays.com/ | head -12", + "curl -sSL --max-time 20 https://exposedgays.com/ | head -c 200", + ] + out = [] + for cmd in cmds: + _, o, e = c.exec_command(cmd, timeout=35) + o.channel.recv_exit_status() + out.append(f"$ {cmd}\n" + (o.read() + e.read()).decode(errors="replace")[:1500]) + c.close() + return "\n\n".join(out) + + +def main(): + token = get_token() + code, recs_body = cf(token, f"/zones/{ZID}/dns_records?per_page=100") + if not recs_body.get("success"): + raise SystemExit(f"list records failed: {recs_body.get('errors')}") + records = recs_body["result"] + + for name in HOSTS: + body = {"type": "A", "name": name, "content": ORIGIN_IP, "ttl": 1, "proxied": True} + existing = next( + (r for r in records if r["name"] == name and r["type"] in ("A", "CNAME", "AAAA")), + None, + ) + if existing: + code, res = cf(token, f"/zones/{ZID}/dns_records/{existing['id']}", "PATCH", body) + action = "PATCH" + else: + code, res = cf(token, f"/zones/{ZID}/dns_records", "POST", body) + action = "POST" + print(f"{action} {name}: HTTP {code} success={res.get('success')} errors={res.get('errors')}") + if not res.get("success"): + raise SystemExit(1) + + print("\nFinal apex/www records:") + _, final_body = cf(token, f"/zones/{ZID}/dns_records?per_page=100") + for r in final_body.get("result", []): + if r["name"] in HOSTS: + print(f" {r['type']} {r['name']} -> {r['content']} proxied={r.get('proxied')}") + + print("\nWaiting 15s for DNS/proxy propagation...") + time.sleep(15) + print("\n=== verify ===") + print(ssh_check()) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) \ No newline at end of file diff --git a/deploy/fix-tunnel-ingress.py b/deploy/fix-tunnel-ingress.py new file mode 100644 index 0000000..68c7b92 --- /dev/null +++ b/deploy/fix-tunnel-ingress.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 +"""Add exposedgays.com to Cloudflare tunnel remote ingress + verify DNS.""" +import json +import re +import time +import urllib.error +import urllib.request +import paramiko + +PASS = "Bbt9115xty9176!" +ACCOUNT = "2599c23bbb1255dbb73e8d34b4115fda" +TUNNEL_ID = "03079a26-f14c-4622-b463-ba54a24f7472" +HOSTS = ["exposedgays.com", "www.exposedgays.com"] +RULES = [ + { + "hostname": "exposedgays.com", + "service": "https://localhost:443", + "originRequest": {"noTLSVerify": True}, + }, + { + "hostname": "www.exposedgays.com", + "service": "https://localhost:443", + "originRequest": {"noTLSVerify": True}, + }, +] + + +def get_token(): + c = paramiko.SSHClient() + c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + c.connect("10.10.0.1", username="admin", password=PASS, timeout=15) + _, o, e = c.exec_command("cat /usr/local/etc/wan-dns-failover.env", timeout=30) + o.channel.recv_exit_status() + text = (o.read() + e.read()).decode() + c.close() + return re.search(r"CLOUDFLARE_API_TOKEN=([^\s\"']+)", text).group(1) + + +def cf(token, path, method="GET", data=None): + req = urllib.request.Request( + f"https://api.cloudflare.com/client/v4{path}", + data=json.dumps(data).encode() if data is not None else None, + method=method, + headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"}, + ) + try: + with urllib.request.urlopen(req, timeout=60) as r: + return r.status, json.load(r) + except urllib.error.HTTPError as e: + return e.code, json.loads(e.read().decode()) + + +def ssh_run(cmd, timeout=90): + c = paramiko.SSHClient() + c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + c.connect("10.10.0.10", username="localadministrator", password=PASS, timeout=30) + _, o, e = c.exec_command(cmd, timeout=timeout) + o.channel.recv_exit_status() + out = (o.read() + e.read()).decode(errors="replace") + c.close() + return out + + +def merge_ingress(ingress): + hostnames = {i.get("hostname") for i in ingress if i.get("hostname")} + changed = False + for rule in RULES: + if rule["hostname"] not in hostnames: + catch = next((i for i, x in enumerate(ingress) if not x.get("hostname")), len(ingress)) + ingress.insert(catch, rule) + hostnames.add(rule["hostname"]) + changed = True + print("added", rule["hostname"]) + return ingress, changed + + +def main(): + token = get_token() + + code, body = cf(token, f"/accounts/{ACCOUNT}/cfd_tunnel/{TUNNEL_ID}/configurations") + print(f"GET tunnel config HTTP {code}") + if not body.get("result"): + print(body.get("errors")) + return 1 + + ingress = body["result"].get("config", {}).get("ingress", []) + present = [i.get("hostname") for i in ingress if i.get("hostname") in HOSTS] + print("exposedgays in remote ingress:", present) + + ingress, changed = merge_ingress(ingress) + if changed: + put_code, put = cf( + token, + f"/accounts/{ACCOUNT}/cfd_tunnel/{TUNNEL_ID}/configurations", + "PUT", + {"config": {"ingress": ingress, "warp-routing": {"enabled": False}}}, + ) + print(f"PUT HTTP {put_code} success={put.get('success')} errors={put.get('errors')}") + if not put.get("success"): + return 1 + print(ssh_run(f"echo '{PASS}' | sudo -S systemctl restart cloudflared")) + time.sleep(8) + + print("\n=== verify ===") + print( + ssh_run( + "systemctl is-active cloudflared; " + "dig @1.1.1.1 www.exposedgays.com CNAME +short; " + "curl -sSI --max-time 20 https://www.exposedgays.com/ | head -12; " + "curl -sSL --max-time 20 https://www.exposedgays.com/ | head -c 250" + ) + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) \ No newline at end of file diff --git a/deploy/force-godaddy-ns-cf.py b/deploy/force-godaddy-ns-cf.py new file mode 100644 index 0000000..c0205b0 --- /dev/null +++ b/deploy/force-godaddy-ns-cf.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 +"""Force GoDaddy registrar NS to Cloudflare.""" +import json +import time +import urllib.error +import urllib.request + +KEY = "2wYVpc3VT4_Gi9AsNqzJ6BtSfNz6nLDuo" +SECRET = "NvC3qexGAU7Rd5QExUTH6z" +DOMAIN = "exposedgays.com" +CF_NS = ["annabel.ns.cloudflare.com", "mitchell.ns.cloudflare.com"] +GD = "https://api.godaddy.com/v1" + + +def gd(path, method="GET", body=None): + req = urllib.request.Request( + GD + path, + data=json.dumps(body).encode() if body is not None else None, + method=method, + headers={ + "Authorization": f"sso-key {KEY}:{SECRET}", + "Content-Type": "application/json", + "Accept": "application/json", + }, + ) + try: + with urllib.request.urlopen(req, timeout=45) as r: + raw = r.read().decode() + return r.status, json.loads(raw) if raw else {} + except urllib.error.HTTPError as e: + raw = e.read().decode() + try: + return e.code, json.loads(raw) + except Exception: + return e.code, {"raw": raw[:500]} + + +code, d = gd(f"/domains/{DOMAIN}") +print("Before:", d.get("nameServers")) + +# Try PUT instead of PATCH +code, res = gd(f"/domains/{DOMAIN}", "PATCH", {"nameServers": CF_NS, "locked": False}) +print(f"PATCH HTTP {code}: {res}") + +time.sleep(3) +code, d = gd(f"/domains/{DOMAIN}") +print("After:", d.get("nameServers")) + +# Also try nameservers endpoint if exists +code, res = gd(f"/domains/{DOMAIN}/nameservers", "PUT", CF_NS) +print(f"PUT /nameservers HTTP {code}: {res}") + +time.sleep(3) +code, d = gd(f"/domains/{DOMAIN}") +print("Final:", d.get("nameServers")) \ No newline at end of file diff --git a/deploy/get-log.py b/deploy/get-log.py index 3ddad0e..c381738 100644 --- a/deploy/get-log.py +++ b/deploy/get-log.py @@ -5,7 +5,13 @@ c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) c.connect("10.10.0.10", username="localadministrator", password="Bbt9115xty9176!", timeout=20) _, o, e = c.exec_command( "docker exec coolify-db psql -U coolify -d coolify -t -A -c " - "\"SELECT logs FROM application_deployment_queues WHERE id=1086;\"" + "\"SELECT logs FROM application_deployment_queues WHERE id=1088;\"" ) -print(o.read().decode()[-15000:]) +text = o.read().decode() +# find error lines +for needle in ["Type error", "Failed to compile", "ERROR:", "error"]: + idx = text.rfind(needle) + if idx >= 0: + print(text[max(0, idx-500):idx+1500]) + print("---") c.close() \ No newline at end of file diff --git a/deploy/godaddy-delegate-now.py b/deploy/godaddy-delegate-now.py new file mode 100644 index 0000000..75a1c1c --- /dev/null +++ b/deploy/godaddy-delegate-now.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python3 +"""Point GoDaddy NS at Cloudflare (account default NS) + set CF DNS when zone exists.""" +import json +import re +import time +import urllib.error +import urllib.request +import paramiko + +PASS = "Bbt9115xty9176!" +DOMAIN = "exposedgays.com" +ACCOUNT = "2599c23bbb1255dbb73e8d34b4115fda" +TUNNEL_CNAME = "03079a26-f14c-4622-b463-ba54a24f7472.cfargotunnel.com" +# Same NS pair used by justtworoommates.com, thepinkpulse.com, freerangefruit.com on this CF account +CF_NS = ["annabel.ns.cloudflare.com", "mitchell.ns.cloudflare.com"] +CF = "https://api.cloudflare.com/client/v4" +GD = "https://api.godaddy.com/v1" +GODADDY_KEY = "2wYVpc3VT4_Gi9AsNqzJ6BtSfNz6nLDuo" +GODADDY_SECRET = "NvC3qexGAU7Rd5QExUTH6z" + + +def load_cf_token(): + c = paramiko.SSHClient() + c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + c.connect("10.10.0.1", username="admin", password=PASS, timeout=15) + _, o, e = c.exec_command("cat /usr/local/etc/wan-dns-failover.env", timeout=30) + o.channel.recv_exit_status() + text = (o.read() + e.read()).decode() + c.close() + return re.search(r"CLOUDFLARE_API_TOKEN=([^\s\"']+)", text).group(1) + + +def cf(tok, path, method="GET", body=None): + req = urllib.request.Request( + CF + path, + data=json.dumps(body).encode() if body is not None else None, + method=method, + headers={"Authorization": f"Bearer {tok}", "Content-Type": "application/json"}, + ) + try: + with urllib.request.urlopen(req, timeout=45) as r: + return json.load(r) + except urllib.error.HTTPError as e: + return json.loads(e.read().decode()) + + +def gd(path, method="GET", body=None): + req = urllib.request.Request( + GD + path, + data=json.dumps(body).encode() if body is not None else None, + method=method, + headers={ + "Authorization": f"sso-key {GODADDY_KEY}:{GODADDY_SECRET}", + "Content-Type": "application/json", + "Accept": "application/json", + }, + ) + try: + with urllib.request.urlopen(req, timeout=45) as r: + raw = r.read().decode() + return r.status, json.loads(raw) if raw else {} + except urllib.error.HTTPError as e: + raw = e.read().decode() + try: + return e.code, json.loads(raw) + except Exception: + return e.code, {"raw": raw[:800]} + + +def find_zone(tok): + res = cf(tok, f"/zones?name={DOMAIN}") + return res["result"][0] if res.get("result") else None + + +def upsert_tunnel_cnames(tok, zid): + records = cf(tok, f"/zones/{zid}/dns_records?per_page=100").get("result", []) + for name in [DOMAIN, f"www.{DOMAIN}"]: + body = {"type": "CNAME", "name": name, "content": TUNNEL_CNAME, "ttl": 1, "proxied": True} + existing = next( + (r for r in records if r["name"] == name and r["type"] in ("A", "CNAME", "AAAA")), + None, + ) + if existing: + res = cf(tok, f"/zones/{zid}/dns_records/{existing['id']}", "PATCH", body) + action = "PATCH" + else: + res = cf(tok, f"/zones/{zid}/dns_records", "POST", body) + action = "POST" + print(f" CF {action} {name}: success={res.get('success')} errors={res.get('errors')}") + + +def verify(): + c = paramiko.SSHClient() + c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + c.connect("10.10.0.10", username="localadministrator", password=PASS, timeout=30) + for cmd in [ + f"dig @1.1.1.1 {DOMAIN} NS +short", + f"dig @1.1.1.1 www.{DOMAIN} A +short", + f"curl -sSI https://www.{DOMAIN}/ | head -8", + f"curl -sSL https://www.{DOMAIN}/ | head -c 200", + ]: + print(f"\n$ {cmd}") + _, o, e = c.exec_command(cmd, timeout=30) + o.channel.recv_exit_status() + print((o.read() + e.read()).decode(errors="replace")) + c.close() + + +def main(): + print("=== GoDaddy before ===") + code, before = gd(f"/domains/{DOMAIN}") + print(f"HTTP {code} NS={before.get('nameServers') if code == 200 else before}") + + print(f"\n=== GoDaddy PATCH NS -> {CF_NS} ===") + code, res = gd(f"/domains/{DOMAIN}", "PATCH", {"nameServers": CF_NS}) + print(f"HTTP {code}: {json.dumps(res, indent=2) if res else '{}'}") + + code, after = gd(f"/domains/{DOMAIN}") + if code == 200: + print(f"GoDaddy after NS={after.get('nameServers')}") + + tok = load_cf_token() + zone = find_zone(tok) + if not zone: + created = cf( + tok, + "/zones", + "POST", + {"name": DOMAIN, "account": {"id": ACCOUNT}, "type": "full", "jump_start": False}, + ) + if created.get("success"): + zone = created["result"] + print("\nCreated CF zone via API") + else: + print("\nCF zone not in account yet (API create still denied).") + print("Add exposedgays.com at https://dash.cloudflare.com — NS already points at CF.") + print("Then re-run: python exposedgays/deploy/delegate-godaddy-to-cf.py") + else: + print(f"\nCF zone found: {zone['id']} status={zone['status']}") + print("Setting tunnel CNAME records...") + upsert_tunnel_cnames(tok, zone["id"]) + + print("\nWaiting 20s...") + time.sleep(20) + verify() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/deploy/godaddy-dns-tunnel.py b/deploy/godaddy-dns-tunnel.py new file mode 100644 index 0000000..54f42f3 --- /dev/null +++ b/deploy/godaddy-dns-tunnel.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +"""Point exposedgays.com DNS at Cloudflare tunnel via GoDaddy API.""" +import json +import time +import urllib.error +import urllib.request +import paramiko + +GODADDY_KEY = "2wYVpc3VT4_Gi9AsNqzJ6BtSfNz6nLDuo" +GODADDY_SECRET = "NvC3qexGAU7Rd5QExUTH6z" +DOMAIN = "exposedgays.com" +TUNNEL_CNAME = "03079a26-f14c-4622-b463-ba54a24f7472.cfargotunnel.com" +GD = "https://api.godaddy.com/v1" +PASS = "Bbt9115xty9176!" + + +def gd(path, method="GET", body=None): + req = urllib.request.Request( + GD + path, + data=json.dumps(body).encode() if body is not None else None, + method=method, + headers={ + "Authorization": f"sso-key {GODADDY_KEY}:{GODADDY_SECRET}", + "Content-Type": "application/json", + "Accept": "application/json", + }, + ) + try: + with urllib.request.urlopen(req, timeout=45) as r: + raw = r.read().decode() + return r.status, json.loads(raw) if raw else {} + except urllib.error.HTTPError as e: + raw = e.read().decode() + try: + return e.code, json.loads(raw) + except Exception: + return e.code, {"raw": raw[:800]} + + +def show_records(label): + print(f"\n=== {label} ===") + code, records = gd(f"/domains/{DOMAIN}/records") + print("HTTP", code) + for r in records if isinstance(records, list) else []: + print(f" {r['type']:5} {r['name']:20} -> {r.get('data')}") + + +def main(): + show_records("Before") + + # Full replace must retain NS records. + new_records = [ + {"type": "NS", "name": "@", "data": "ns71.domaincontrol.com", "ttl": 3600}, + {"type": "NS", "name": "@", "data": "ns72.domaincontrol.com", "ttl": 3600}, + {"type": "CNAME", "name": "www", "data": TUNNEL_CNAME, "ttl": 600}, + {"type": "CNAME", "name": "@", "data": TUNNEL_CNAME, "ttl": 600}, + { + "type": "TXT", + "name": "_dmarc", + "data": "v=DMARC1; p=quarantine; adkim=r; aspf=r; rua=mailto:dmarc_rua@onsecureserver.net;", + "ttl": 3600, + }, + ] + + print("\n=== PUT full record set ===") + code, res = gd(f"/domains/{DOMAIN}/records", "PUT", new_records) + print("HTTP", code, res) + + if code not in (200, 204): + print("\n=== Per-record fallback ===") + # Delete parked apex A + code_del, res_del = gd(f"/domains/{DOMAIN}/records/A/@", "DELETE") + print("DELETE A @", code_del, res_del) + + # Upsert www CNAME + code_www, res_www = gd( + f"/domains/{DOMAIN}/records/CNAME/www", + "PUT", + [{"data": TUNNEL_CNAME, "ttl": 600}], + ) + print("PUT CNAME www", code_www, res_www) + + # Try apex CNAME + code_apex, res_apex = gd( + f"/domains/{DOMAIN}/records/CNAME/@", + "PUT", + [{"data": TUNNEL_CNAME, "ttl": 600}], + ) + print("PUT CNAME @", code_apex, res_apex) + + show_records("After") + time.sleep(8) + + print("\n=== Verify from COOLIFY_01 ===") + c = paramiko.SSHClient() + c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + c.connect("10.10.0.10", username="localadministrator", password=PASS, timeout=30) + cmds = [ + f"dig @1.1.1.1 {DOMAIN} CNAME +short", + f"dig @1.1.1.1 www.{DOMAIN} CNAME +short", + f"curl -sSI https://www.{DOMAIN}/ | head -12", + f"curl -sSL https://www.{DOMAIN}/ | head -c 400", + f"curl -sSI https://{DOMAIN}/ | head -12", + ] + for cmd in cmds: + print(f"\n$ {cmd}") + _, o, e = c.exec_command(cmd, timeout=30) + o.channel.recv_exit_status() + print((o.read() + e.read()).decode(errors="replace")) + c.close() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/deploy/godaddy-exposedgays.py b/deploy/godaddy-exposedgays.py new file mode 100644 index 0000000..3c801eb --- /dev/null +++ b/deploy/godaddy-exposedgays.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 +"""Check GoDaddy domain status for exposedgays.com.""" +import json +import urllib.error +import urllib.request + +KEY = "2wYVpc3VT4_Gi9AsNqzJ6BtSfNz6nLDuo" +SECRET = "NvC3qexGAU7Rd5QExUTH6z" +DOMAIN = "exposedgays.com" +BASE = "https://api.godaddy.com/v1" + + +def gd(path, method="GET", body=None): + req = urllib.request.Request( + BASE + path, + data=json.dumps(body).encode() if body else None, + method=method, + headers={ + "Authorization": f"sso-key {KEY}:{SECRET}", + "Content-Type": "application/json", + "Accept": "application/json", + }, + ) + try: + with urllib.request.urlopen(req, timeout=45) as r: + return r.status, json.load(r) + except urllib.error.HTTPError as e: + try: + return e.code, json.loads(e.read().decode()) + except Exception: + return e.code, {"raw": e.read().decode()[:500]} + + +for path in [ + f"/domains/{DOMAIN}", + f"/domains/{DOMAIN}/records", + f"/domains/{DOMAIN}/records/A", + f"/domains/{DOMAIN}/records/CNAME", +]: + code, res = gd(path) + print(f"\n{path} -> HTTP {code}") + print(json.dumps(res, indent=2)[:3000]) \ No newline at end of file diff --git a/deploy/godaddy-ns-patch.py b/deploy/godaddy-ns-patch.py new file mode 100644 index 0000000..61cdac4 --- /dev/null +++ b/deploy/godaddy-ns-patch.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +"""PATCH GoDaddy nameservers with consent payload.""" +import json +import urllib.error +import urllib.request +from datetime import datetime, timezone + +KEY = "2wYVpc3VT4_Gi9AsNqzJ6BtSfNz6nLDuo" +SECRET = "NvC3qexGAU7Rd5QExUTH6z" +DOMAIN = "exposedgays.com" +CF_NS = ["annabel.ns.cloudflare.com", "mitchell.ns.cloudflare.com"] +GD = "https://api.godaddy.com/v1" + + +def gd(path, method="GET", body=None): + req = urllib.request.Request( + GD + path, + data=json.dumps(body).encode() if body is not None else None, + method=method, + headers={ + "Authorization": f"sso-key {KEY}:{SECRET}", + "Content-Type": "application/json", + "Accept": "application/json", + }, + ) + try: + with urllib.request.urlopen(req, timeout=45) as r: + raw = r.read().decode() + return r.status, json.loads(raw) if raw else {} + except urllib.error.HTTPError as e: + raw = e.read().decode() + try: + return e.code, json.loads(raw) + except Exception: + return e.code, {"raw": raw[:1000]} + + +def show(label): + code, d = gd(f"/domains/{DOMAIN}") + print(f"{label}: HTTP {code} NS={d.get('nameServers') if code == 200 else d}") + + +show("Before") + +bodies = [ + {"nameServers": CF_NS}, + { + "nameServers": CF_NS, + "consent": { + "agreedAt": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.000Z"), + "agreedBy": "96.94.95.105", + "agreementKeys": ["DNRA"], + }, + }, + { + "nameServers": CF_NS, + "locked": False, + "consent": { + "agreedAt": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.000Z"), + "agreedBy": "0.0.0.0", + "agreementKeys": ["DNRA", "DNPA"], + }, + }, +] + +for i, body in enumerate(bodies, 1): + print(f"\n=== PATCH attempt {i} ===") + print(json.dumps(body, indent=2)) + code, res = gd(f"/domains/{DOMAIN}", "PATCH", body) + print(f"HTTP {code}: {json.dumps(res, indent=2) if res else '{}'}") + +show("After immediate") +import time +time.sleep(5) +show("After 5s") \ No newline at end of file diff --git a/deploy/godaddy-ns-to-cf.py b/deploy/godaddy-ns-to-cf.py new file mode 100644 index 0000000..ee2f568 --- /dev/null +++ b/deploy/godaddy-ns-to-cf.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +"""Delegate exposedgays.com from GoDaddy to Cloudflare nameservers.""" +import json +import re +import urllib.error +import urllib.request + +GODADDY_KEY = "2wYVpc3VT4_Gi9AsNqzJ6BtSfNz6nLDuo" +GODADDY_SECRET = "NvC3qexGAU7Rd5QExUTH6z" +DOMAIN = "exposedgays.com" +GD = "https://api.godaddy.com/v1" + + +def gd(path, method="GET", body=None): + req = urllib.request.Request( + GD + path, + data=json.dumps(body).encode() if body is not None else None, + method=method, + headers={ + "Authorization": f"sso-key {GODADDY_KEY}:{GODADDY_SECRET}", + "Content-Type": "application/json", + "Accept": "application/json", + }, + ) + try: + with urllib.request.urlopen(req, timeout=45) as r: + raw = r.read().decode() + return r.status, json.loads(raw) if raw else {} + except urllib.error.HTTPError as e: + raw = e.read().decode() + try: + return e.code, json.loads(raw) + except Exception: + return e.code, {"raw": raw[:800]} + + +def main(): + code, domain = gd(f"/domains/{DOMAIN}") + print(f"Domain GET HTTP {code}") + if code == 200: + print(f" Current NS: {domain.get('nameServers')}") + print(f" Status: {domain.get('status')}") + + # Show what we'd set — caller passes NS via env or args in wrapper + import sys + if len(sys.argv) < 2: + print("\nUsage: godaddy-ns-to-cf.py [ns3...]") + return 1 + + nameservers = sys.argv[1:] + print(f"\nPATCH nameservers -> {nameservers}") + code, res = gd(f"/domains/{DOMAIN}", "PATCH", {"nameServers": nameservers}) + print(f"HTTP {code}: {json.dumps(res, indent=2)}") + + code, domain = gd(f"/domains/{DOMAIN}") + if code == 200: + print(f"\nAfter PATCH NS: {domain.get('nameServers')}") + return 0 if code == 200 else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) \ No newline at end of file diff --git a/deploy/godaddy-probe-connect.py b/deploy/godaddy-probe-connect.py new file mode 100644 index 0000000..c774a73 --- /dev/null +++ b/deploy/godaddy-probe-connect.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +import json +import urllib.error +import urllib.request + +KEY = "2wYVpc3VT4_Gi9AsNqzJ6BtSfNz6nLDuo" +SECRET = "NvC3qexGAU7Rd5QExUTH6z" +DOMAIN = "exposedgays.com" +BASE = "https://api.godaddy.com/v1" + + +def gd(path, method="GET", body=None): + req = urllib.request.Request( + BASE + path, + data=json.dumps(body).encode() if body else None, + method=method, + headers={ + "Authorization": f"sso-key {KEY}:{SECRET}", + "Content-Type": "application/json", + "Accept": "application/json", + }, + ) + try: + with urllib.request.urlopen(req, timeout=30) as r: + raw = r.read().decode() + return r.status, json.loads(raw) if raw else {} + except urllib.error.HTTPError as e: + raw = e.read().decode() + try: + return e.code, json.loads(raw) + except Exception: + return e.code, {"raw": raw[:500]} + + +paths = [ + f"/domains/{DOMAIN}/connect", + f"/domains/{DOMAIN}/connect/cloudflare", + f"/domains/{DOMAIN}/suggest", + f"/domains/{DOMAIN}/records/NS/@", +] +for p in paths: + code, res = gd(p) + print(f"{p} -> {code}") + print(json.dumps(res, indent=2)[:800]) + print() \ No newline at end of file diff --git a/deploy/grep-remote-config.py b/deploy/grep-remote-config.py new file mode 100644 index 0000000..adfcaf6 --- /dev/null +++ b/deploy/grep-remote-config.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python3 +import paramiko +PASS = "Bbt9115xty9176!" +c = paramiko.SSHClient() +c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +c.connect("10.10.0.10", username="localadministrator", password=PASS, timeout=30) +_, o, e = c.exec_command( + "sudo journalctl -u cloudflared --no-pager -n 50 | grep 'Updated to new configuration' | tail -1", + timeout=30, +) +o.channel.recv_exit_status() +line = (o.read() + e.read()).decode() +print("exposedgays in remote:", "exposedgays" in line) +for h in ["exposedgays.com", "www.exposedgays.com", "thepinkpulse.com", "avbeat.com"]: + print(f" {h}: {h in line}") +# restart to pick up dashboard changes +_, o2, e2 = c.exec_command(f"echo '{PASS}' | sudo -S systemctl restart cloudflared && sleep 8 && sudo journalctl -u cloudflared --no-pager -n 8", timeout=60) +o2.channel.recv_exit_status() +out = (o2.read() + e2.read()).decode() +print("\nAfter restart:") +print("exposedgays:", "exposedgays" in out) +for cmd in [ + "curl -sSI --max-time 25 https://exposedgays.com/ 2>&1 | head -15", + "curl -sSI --max-time 25 https://www.exposedgays.com/ 2>&1 | head -15", + "curl -sSL --max-time 25 https://www.exposedgays.com/ 2>&1 | head -c 350", +]: + print(f"\n=== {cmd} ===") + _, o, e = c.exec_command(cmd, timeout=35) + o.channel.recv_exit_status() + print((o.read() + e.read()).decode(errors="replace")[:2000]) +c.close() \ No newline at end of file diff --git a/deploy/list-cf-zones.py b/deploy/list-cf-zones.py new file mode 100644 index 0000000..db54efe --- /dev/null +++ b/deploy/list-cf-zones.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python3 +import json +import re +import urllib.request +import paramiko + +PASS = "Bbt9115xty9176!" +c = paramiko.SSHClient() +c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +c.connect("10.10.0.1", username="admin", password=PASS, timeout=15) +_, o, e = c.exec_command("cat /usr/local/etc/wan-dns-failover.env", timeout=30) +o.channel.recv_exit_status() +TOK = re.search(r"CLOUDFLARE_API_TOKEN=([^\s\"']+)", (o.read() + e.read()).decode()).group(1) +c.close() + +req = urllib.request.Request( + "https://api.cloudflare.com/client/v4/zones?per_page=50", + headers={"Authorization": f"Bearer {TOK}", "Content-Type": "application/json"}, +) +with urllib.request.urlopen(req, timeout=30) as r: + zones = json.load(r)["result"] + +for z in sorted(zones, key=lambda x: x["name"]): + mark = " <--" if "exposed" in z["name"] or "gay" in z["name"] else "" + print(f"{z['name']:35} {z['status']:10} {z['id']}{mark}") \ No newline at end of file diff --git a/deploy/move-to-localhost.py b/deploy/move-to-localhost.py new file mode 100644 index 0000000..297e07a --- /dev/null +++ b/deploy/move-to-localhost.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +import paramiko + +HOST = "10.10.0.10" +USER = "localadministrator" +PASS = "Bbt9115xty9176!" +APP = "ira4bw4nhbvm87o7s3fgqu6v" + +c = paramiko.SSHClient() +c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +c.connect(HOST, username=USER, password=PASS, timeout=20, allow_agent=False, look_for_keys=False) + + +def run(cmd, t=300): + print("$", cmd[:150]) + _, o, e = c.exec_command(cmd, timeout=t) + code = o.channel.recv_exit_status() + out = (o.read() + e.read()).decode(errors="replace") + print(out[-6000:]) + print("exit", code, "\n") + return code, out + + +# Move to localhost destination (id=0) +run( + "docker exec coolify-db psql -U coolify -d coolify -c " + "\"UPDATE applications SET destination_id=0 WHERE uuid='ira4bw4nhbvm87o7s3fgqu6v'; " + "SELECT destination_id,fqdn FROM applications WHERE uuid='ira4bw4nhbvm87o7s3fgqu6v';\"" +) + +run( + "TOKEN=$(cat /home/localadministrator/.coolify-token | tail -1); " + f"curl -sS -X POST 'http://127.0.0.1:8000/api/v1/deploy?uuid={APP}&force_rebuild=true' " + '-H "Authorization: Bearer $TOKEN"' +) + +for i in range(120): + _, out = run("docker exec coolify php artisan queue:work --queue=high,default --once --timeout=900 2>&1", t=920) + if "ApplicationDeploymentJob" in out and ("DONE" in out or "FAIL" in out): + break + +run("docker ps --format '{{.Names}} {{.Status}}' | grep ira4 || true") +run("curl -sS -o /dev/null -w '%{http_code}' -k -H 'Host: exposedgays.com' https://127.0.0.1/") +c.close() \ No newline at end of file diff --git a/deploy/probe-all-cf-tokens.py b/deploy/probe-all-cf-tokens.py new file mode 100644 index 0000000..b07c2c2 --- /dev/null +++ b/deploy/probe-all-cf-tokens.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +import json +import re +import urllib.error +import urllib.request +import paramiko + +PASS = "Bbt9115xty9176!" +ACCOUNT = "2599c23bbb1255dbb73e8d34b4115fda" +TUNNEL = "03079a26-f14c-4622-b463-ba54a24f7472" +CF = "https://api.cloudflare.com/client/v4" + +tokens = {} + +c = paramiko.SSHClient() +c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +c.connect("10.10.0.1", username="admin", password=PASS, timeout=15) +_, o, e = c.exec_command("cat /usr/local/etc/wan-dns-failover.env", timeout=30) +o.channel.recv_exit_status() +text = (o.read() + e.read()).decode() +tokens["pfsense"] = re.search(r"CLOUDFLARE_API_TOKEN=([^\s\"']+)", text).group(1) +c.close() + +c2 = paramiko.SSHClient() +c2.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +c2.connect("10.10.0.10", username="localadministrator", password=PASS, timeout=30) +for path in [ + "/home/localadministrator/coder/onsethost/.env.local", + "/etc/infra/mail-watchdog.env", + "/etc/infra/secrets.env", +]: + _, o, e = c2.exec_command( + f"echo '{PASS}' | sudo -S grep -h CLOUDFLARE_API_TOKEN {path} 2>/dev/null | head -1", + timeout=20, + ) + o.channel.recv_exit_status() + line = (o.read() + e.read()).decode().strip() + if "CLOUDFLARE_API_TOKEN=" in line: + tokens[path] = line.split("=", 1)[1].strip().strip('"') +c2.close() + +tokens["hardcoded_mail"] = "cfut_zlUt5lAKVsu7qIcRCHJnyhAvpJVF5jx24HlrIEn9a8fcd6a1" + + +def probe(label, tok): + results = {} + for name, path, method in [ + ("zone_list", "/zones?per_page=1", "GET"), + ("zone_create", "/zones", "POST"), + ("tunnel_cfg", f"/accounts/{ACCOUNT}/cfd_tunnel/{TUNNEL}/configurations", "GET"), + ]: + body = None + if name == "zone_create": + body = {"name": "exposedgays.com", "account": {"id": ACCOUNT}, "type": "full"} + req = urllib.request.Request( + CF + path, + data=json.dumps(body).encode() if body else None, + method=method if name != "zone_list" else "GET", + headers={"Authorization": f"Bearer {tok}", "Content-Type": "application/json"}, + ) + try: + with urllib.request.urlopen(req, timeout=30) as r: + data = json.load(r) + results[name] = ("OK", data.get("success"), len(data.get("result") or [])) + except urllib.error.HTTPError as e: + err = json.loads(e.read().decode()) + results[name] = (e.code, err.get("errors", [{}])[0].get("message", "")[:80]) + print(label, results) + + +for label, tok in tokens.items(): + if tok: + probe(label, tok) \ No newline at end of file diff --git a/deploy/read-cf-config.py b/deploy/read-cf-config.py new file mode 100644 index 0000000..fd824bc --- /dev/null +++ b/deploy/read-cf-config.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python3 +import paramiko +PASS = "Bbt9115xty9176!" +c = paramiko.SSHClient() +c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +c.connect("10.10.0.10", username="localadministrator", password=PASS, timeout=30) +_, o, e = c.exec_command("sudo cat /etc/cloudflared/config.yml", timeout=30) +o.channel.recv_exit_status() +print((o.read() + e.read()).decode()) +c.close() \ No newline at end of file diff --git a/deploy/read-cloudflared-service.py b/deploy/read-cloudflared-service.py new file mode 100644 index 0000000..4787bc5 --- /dev/null +++ b/deploy/read-cloudflared-service.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python3 +import paramiko +PASS = "Bbt9115xty9176!" +c = paramiko.SSHClient() +c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +c.connect("10.10.0.10", username="localadministrator", password=PASS, timeout=30) +for cmd in [ + "sudo cat /etc/systemd/system/cloudflared.service", + "cloudflared --version", + "cloudflared tunnel run --help 2>&1 | head -40", +]: + print(f"\n=== {cmd} ===") + _, o, e = c.exec_command(cmd, timeout=30) + o.channel.recv_exit_status() + print((o.read() + e.read()).decode(errors="replace")[:4000]) +c.close() \ No newline at end of file diff --git a/deploy/read-tunnel.py b/deploy/read-tunnel.py new file mode 100644 index 0000000..e474667 --- /dev/null +++ b/deploy/read-tunnel.py @@ -0,0 +1,9 @@ +#!/usr/bin/env python3 +import paramiko +c = paramiko.SSHClient() +c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +c.connect("10.10.0.10", username="localadministrator", password="Bbt9115xty9176!", timeout=20) +_, o, e = c.exec_command("sudo head -30 /etc/cloudflared/config.yml; echo '---'; sudo cat /etc/cloudflared/*.json 2>/dev/null | head -5; ls -la /etc/cloudflared/ 2>/dev/null", timeout=30) +print(o.read().decode()) +print(e.read().decode()) +c.close() \ No newline at end of file diff --git a/deploy/redeploy.py b/deploy/redeploy.py index 825cf81..75a0069 100644 --- a/deploy/redeploy.py +++ b/deploy/redeploy.py @@ -1,16 +1,10 @@ #!/usr/bin/env python3 -import io -import os -import tarfile import paramiko HOST = "10.10.0.10" USER = "localadministrator" PASS = "Bbt9115xty9176!" -GITEA_TOKEN = "5c323b8ea677d78b201341f6833d57156a607008" APP = "ira4bw4nhbvm87o7s3fgqu6v" -PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -GIT_AUTH = f"https://{USER}:{GITEA_TOKEN}@gitea.onsethost.com/localadministrator/exposedgays.git" c = paramiko.SSHClient() c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) @@ -22,58 +16,38 @@ def run(cmd, t=300): _, o, e = c.exec_command(cmd, timeout=t) code = o.channel.recv_exit_status() out = (o.read() + e.read()).decode(errors="replace") - print(out[-10000:]) + print(out[-5000:]) print("exit", code, "\n") return code, out -# Upload updated tarball -buf = io.BytesIO() -with tarfile.open(fileobj=buf, mode="w:gz") as tar: - for root, dirs, files in os.walk(PROJECT_ROOT): - dirs[:] = [d for d in dirs if d not in (".next", "node_modules", ".git")] - for f in files: - if f in (".env.local",): - continue - full = os.path.join(root, f) - arc = os.path.relpath(full, PROJECT_ROOT) - tar.add(full, arcname=arc) -buf.seek(0) -sftp = c.open_sftp() -sftp.putfo(buf, "/tmp/exposedgays.tar.gz") -sftp.close() - -run("rm -rf /tmp/exposedgays && mkdir -p /tmp/exposedgays && tar -xzf /tmp/exposedgays.tar.gz -C /tmp/exposedgays") -run( - "cd /tmp/exposedgays && git add -A && " - "git commit -m 'Fix Dockerfile: npm install fallback when no lockfile' || true && " - f"git push origin main" -) - -# Get latest deployment log -run( - f"TOKEN=$(cat /home/localadministrator/.coolify-token | tail -1); " - f"curl -sS 'http://127.0.0.1:8000/api/v1/applications/{APP}/deployments' " - f'-H "Authorization: Bearer $TOKEN" | python3 -c "import sys,json;d=json.load(sys.stdin); ' - f'items=d if isinstance(d,list) else d.get(\\\"deployments\\\",d); ' - f'[print(x.get(\\\"deployment_uuid\\\"),x.get(\\\"status\\\"),x.get(\\\"failure_reason\\\")) for x in items[:3]]" 2>/dev/null' -) - run( "TOKEN=$(cat /home/localadministrator/.coolify-token | tail -1); " f"curl -sS -X POST 'http://127.0.0.1:8000/api/v1/deploy?uuid={APP}&force_rebuild=true' " '-H "Authorization: Bearer $TOKEN"' ) -for i in range(30): +success = False +for i in range(200): _, out = run("docker exec coolify php artisan queue:work --queue=high,default --once --timeout=900 2>&1", t=920) - if "ApplicationDeploymentJob" in out and "DONE" in out: - print("DEPLOY DONE") - break - if "ApplicationDeploymentJob" in out and "FAIL" in out: - print("DEPLOY FAIL - fetching logs") - break + if "ApplicationDeploymentJob" in out: + if "DONE" in out: + success = True + print("=== DEPLOY SUCCESS ===") + break + if "FAIL" in out: + print("=== DEPLOY FAILED ===") + break -run("docker ps --format '{{.Names}} {{.Status}}' | grep -i ira4 || docker ps --format '{{.Names}} {{.Status}}' | grep -i exposed || true") +run( + "docker exec coolify-db psql -U coolify -d coolify -c " + "\"SELECT id,status FROM application_deployment_queues WHERE application_id='31' ORDER BY id DESC LIMIT 1;\"" +) +run( + "docker exec coolify-db psql -U coolify -d coolify -c " + "\"SELECT status,name FROM applications WHERE uuid='ira4bw4nhbvm87o7s3fgqu6v';\"" +) +run("ssh -o StrictHostKeyChecking=no localadministrator@10.10.0.115 'docker ps --format \"{{.Names}} {{.Status}}\" | grep ira4' 2>&1") -c.close() \ No newline at end of file +c.close() +print("SUCCESS" if success else "CHECK_LOGS") \ No newline at end of file diff --git a/deploy/revert-ns-godaddy.py b/deploy/revert-ns-godaddy.py new file mode 100644 index 0000000..bed8aa4 --- /dev/null +++ b/deploy/revert-ns-godaddy.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +"""Force GoDaddy nameservers back to domaincontrol.""" +import json +import time +import urllib.error +import urllib.request +from datetime import datetime, timezone + +KEY = "2wYVpc3VT4_Gi9AsNqzJ6BtSfNz6nLDuo" +SECRET = "NvC3qexGAU7Rd5QExUTH6z" +DOMAIN = "exposedgays.com" +GD_NS = ["ns71.domaincontrol.com", "ns72.domaincontrol.com"] +GD = "https://api.godaddy.com/v1" + + +def gd(path, method="GET", body=None): + req = urllib.request.Request( + GD + path, + data=json.dumps(body).encode() if body is not None else None, + method=method, + headers={ + "Authorization": f"sso-key {KEY}:{SECRET}", + "Content-Type": "application/json", + "Accept": "application/json", + }, + ) + try: + with urllib.request.urlopen(req, timeout=45) as r: + raw = r.read().decode() + return r.status, json.loads(raw) if raw else {} + except urllib.error.HTTPError as e: + raw = e.read().decode() + try: + return e.code, json.loads(raw) + except Exception: + return e.code, {"raw": raw[:800]} + + +def show(label): + code, d = gd(f"/domains/{DOMAIN}") + print(f"{label}: HTTP {code} NS={d.get('nameServers') if code == 200 else d}") + + +show("Before") +bodies = [ + {"nameServers": GD_NS}, + { + "nameServers": GD_NS, + "consent": { + "agreedAt": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.000Z"), + "agreedBy": "96.94.95.105", + "agreementKeys": ["DNRA"], + }, + }, +] +for i, body in enumerate(bodies, 1): + print(f"\nPATCH attempt {i}: {body}") + code, res = gd(f"/domains/{DOMAIN}", "PATCH", body) + print(f"HTTP {code}: {res}") + show(f"After attempt {i}") + if code in (200, 204): + ns = gd(f"/domains/{DOMAIN}")[1].get("nameServers", []) + if ns == GD_NS: + print("SUCCESS: NS reverted to GoDaddy") + break + time.sleep(3) \ No newline at end of file diff --git a/deploy/scan-tunnel-tokens.py b/deploy/scan-tunnel-tokens.py new file mode 100644 index 0000000..033f4dd --- /dev/null +++ b/deploy/scan-tunnel-tokens.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +"""Find a CF token that can edit tunnel ingress.""" +import json +import re +import urllib.error +import urllib.request +import paramiko + +PASS = "Bbt9115xty9176!" +ACCOUNT = "2599c23bbb1255dbb73e8d34b4115fda" +TUNNEL = "03079a26-f14c-4622-b463-ba54a24f7472" +CF = "https://api.cloudflare.com/client/v4" + +tokens = set() +for host, user in [("10.10.0.1", "admin"), ("10.10.0.10", "localadministrator")]: + c = paramiko.SSHClient() + c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + c.connect(host, username=user, password=PASS, timeout=15) + cmd = ( + f"echo '{PASS}' | sudo -S bash -c \"grep -rho 'cfut_[A-Za-z0-9_-]{{20,}}' " + "/etc/infra /etc/cloudflared /root /home/localadministrator 2>/dev/null | sort -u\"" + if host == "10.10.0.10" + else "grep -o 'cfut_[A-Za-z0-9_-]\\{20,\\}' /usr/local/etc/wan-dns-failover.env" + ) + _, o, e = c.exec_command(cmd, timeout=120) + o.channel.recv_exit_status() + for line in (o.read() + e.read()).decode().splitlines(): + if line.startswith("cfut_"): + tokens.add(line.strip()) + c.close() + +print(f"Scanning {len(tokens)} tokens for tunnel config access...") + +def probe(tok): + req = urllib.request.Request( + f"{CF}/accounts/{ACCOUNT}/cfd_tunnel/{TUNNEL}/configurations", + headers={"Authorization": f"Bearer {tok}", "Content-Type": "application/json"}, + ) + try: + with urllib.request.urlopen(req, timeout=30) as r: + data = json.load(r) + ingress = data.get("result", {}).get("config", {}).get("ingress", []) + eg = [i.get("hostname") for i in ingress if i.get("hostname") and "exposedgays" in i.get("hostname", "")] + return "OK", len(ingress), eg + except urllib.error.HTTPError as e: + err = json.loads(e.read().decode()) + return e.code, err.get("errors", [{}])[0].get("message", "")[:60], [] + +for tok in sorted(tokens): + status, count, eg = probe(tok) + print(f"{tok[:20]}... -> {status} {count} exposedgays={eg}") \ No newline at end of file diff --git a/deploy/setup-cloudflare-dns.py b/deploy/setup-cloudflare-dns.py new file mode 100644 index 0000000..d5ec6f6 --- /dev/null +++ b/deploy/setup-cloudflare-dns.py @@ -0,0 +1,159 @@ +#!/usr/bin/env python3 +"""Add exposedgays.com to Cloudflare + tunnel CNAME records.""" +import json +import re +import time +import urllib.error +import urllib.request +import paramiko + +PASS = "Bbt9115xty9176!" +DOMAIN = "exposedgays.com" +TUNNEL_CNAME = "03079a26-f14c-4622-b463-ba54a24f7472.cfargotunnel.com" +CF_API = "https://api.cloudflare.com/client/v4" +ACCOUNT_ID = "2599c23bbb1255dbb73e8d34b4115fda" + +TOKENS = [] + + +def load_tokens(): + # pfSense production token + try: + c = paramiko.SSHClient() + c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + c.connect("10.10.0.1", username="admin", password=PASS, timeout=15) + _, o, e = c.exec_command("cat /usr/local/etc/wan-dns-failover.env", timeout=30) + o.channel.recv_exit_status() + text = (o.read() + e.read()).decode() + c.close() + m = re.search(r"CLOUDFLARE_API_TOKEN=([^\s\"']+)", text) + if m: + TOKENS.append(("pfsense", m.group(1))) + except Exception as ex: + print("pfsense token:", ex) + + # onsethost env fallback + TOKENS.append(("apply_mail", "cfut_zlUt5lAKVsu7qIcRCHJnyhAvpJVF5jx24HlrIEn9a8fcd6a1")) + TOKENS.append(("onsethost", "cfut_IOm1DJW4pDkPCuQA6UoOjqO6Tcv2zU10y2PSw82m379f7db3")) + + +def cf(token, path, method="GET", data=None): + req = urllib.request.Request( + f"{CF_API}{path}", + data=json.dumps(data).encode() if data is not None else None, + method=method, + headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"}, + ) + try: + with urllib.request.urlopen(req, timeout=45) as r: + return json.load(r) + except urllib.error.HTTPError as e: + return json.loads(e.read().decode()) + + +def find_zone(token): + for q in [f"name={DOMAIN}", f"name={DOMAIN}&status=active", f"name={DOMAIN}&status=pending"]: + res = cf(token, f"/zones?{q}") + if res.get("result"): + return res["result"][0] + # paginate all zones + page = 1 + while page <= 10: + res = cf(token, f"/zones?per_page=50&page={page}") + for z in res.get("result", []): + if z["name"] == DOMAIN: + return z + if len(res.get("result", [])) < 50: + break + page += 1 + return None + + +def create_zone(token): + return cf( + token, + "/zones", + "POST", + {"name": DOMAIN, "account": {"id": ACCOUNT_ID}, "type": "full", "jump_start": False}, + ) + + +def list_records(token, zid): + page, out = 1, [] + while True: + res = cf(token, f"/zones/{zid}/dns_records?per_page=100&page={page}") + batch = res.get("result", []) + out.extend(batch) + if len(batch) < 100: + break + page += 1 + return out + + +def upsert_tunnel_cname(token, zid, existing, name): + body = {"type": "CNAME", "name": name, "content": TUNNEL_CNAME, "ttl": 1, "proxied": True} + if existing: + if existing["type"] == "CNAME" and TUNNEL_CNAME in existing.get("content", "") and existing.get("proxied"): + print(f"OK {name} already on tunnel") + return True + res = cf(token, f"/zones/{zid}/dns_records/{existing['id']}", "PATCH", body) + action = "PATCH" + else: + res = cf(token, f"/zones/{zid}/dns_records", "POST", body) + action = "POST" + print(f"{action} {name}: success={res.get('success')} errors={res.get('errors')}") + return res.get("success") + + +def main(): + load_tokens() + token = None + zone = None + + for label, tok in TOKENS: + z = find_zone(tok) + print(f"[{label}] zone lookup:", "found" if z else "not found") + if z: + token, zone = tok, z + print(f" using {label} token, zone {z['id']} status={z['status']}") + break + + if not zone: + for label, tok in TOKENS: + res = create_zone(tok) + print(f"[{label}] create zone:", res.get("success"), res.get("errors")) + if res.get("success"): + token, zone = tok, res["result"] + break + + if not zone: + raise RuntimeError("Could not find or create exposedgays.com zone with available tokens") + + zid = zone["id"] + nameservers = zone.get("name_servers", []) + status = zone.get("status") + + records = list_records(token, zid) + apex = next((r for r in records if r["name"] == DOMAIN and r["type"] in ("A", "CNAME", "AAAA")), None) + www = next((r for r in records if r["name"] == f"www.{DOMAIN}" and r["type"] in ("A", "CNAME", "AAAA")), None) + print(f"Before apex: {apex and (apex['type'], apex.get('content'))}") + print(f"Before www: {www and (www['type'], www.get('content'))}") + + upsert_tunnel_cname(token, zid, apex, DOMAIN) + upsert_tunnel_cname(token, zid, www, f"www.{DOMAIN}") + + print("\n=== Cloudflare nameservers ===") + for ns in nameservers: + print(f" {ns}") + print(f"Zone status: {status}") + if status != "active": + print("\n>>> Update GoDaddy NS to the Cloudflare nameservers above <<<") + + time.sleep(5) + for r in list_records(token, zid): + if "exposedgays" in r["name"]: + print(f"Final: {r['type']} {r['name']} -> {r['content']} proxied={r.get('proxied')}") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/deploy/test-ingress-rule.py b/deploy/test-ingress-rule.py new file mode 100644 index 0000000..63ff954 --- /dev/null +++ b/deploy/test-ingress-rule.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python3 +import paramiko +PASS = "Bbt9115xty9176!" +c = paramiko.SSHClient() +c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +c.connect("10.10.0.10", username="localadministrator", password=PASS, timeout=30) +for url in ["https://exposedgays.com/", "https://www.exposedgays.com/", "https://thepinkpulse.com/"]: + cmd = f"echo '{PASS}' | sudo -S cloudflared tunnel ingress rule '{url}' --config /etc/cloudflared/config.yml 2>&1" + print(f"\n=== {url} ===") + _, o, e = c.exec_command(cmd, timeout=20) + o.channel.recv_exit_status() + print((o.read()+e.read()).decode()) +c.close() \ No newline at end of file diff --git a/deploy/test-onsethost-subdomain.py b/deploy/test-onsethost-subdomain.py new file mode 100644 index 0000000..9011e21 --- /dev/null +++ b/deploy/test-onsethost-subdomain.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python3 +import paramiko +PASS = "Bbt9115xty9176!" +c = paramiko.SSHClient() +c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +c.connect("10.10.0.10", username="localadministrator", password=PASS, timeout=30) +cmds = [ + "curl -sSI --max-time 10 -k -H 'Host: exposedgays.onsethost.com' https://127.0.0.1/ | head -8", + "curl -sSI --max-time 10 https://exposedgays.onsethost.com/ 2>&1 | head -8", + "dig @1.1.1.1 exposedgays.onsethost.com A +short", +] +for cmd in cmds: + print(f"\n=== {cmd} ===") + _, o, e = c.exec_command(cmd, timeout=20) + o.channel.recv_exit_status() + print((o.read() + e.read()).decode()) +c.close() \ No newline at end of file diff --git a/deploy/test-onsethost-token.py b/deploy/test-onsethost-token.py new file mode 100644 index 0000000..bff81db --- /dev/null +++ b/deploy/test-onsethost-token.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 +import json +import urllib.error +import urllib.request +import paramiko + +PASS = "Bbt9115xty9176!" +ACCOUNT = "2599c23bbb1255dbb73e8d34b4115fda" +TUNNEL = "03079a26-f14c-4622-b463-ba54a24f7472" + +c = paramiko.SSHClient() +c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +c.connect("10.10.0.10", username="localadministrator", password=PASS, timeout=30) +_, o, e = c.exec_command( + "grep CLOUDFLARE_API_TOKEN /home/localadministrator/coder/onsethost/.env.local", + timeout=20, +) +o.channel.recv_exit_status() +line = (o.read() + e.read()).decode().strip() +tok = line.split("=", 1)[1].strip() +c.close() + +print("token prefix:", tok[:12], "len:", len(tok)) + +for path in [ + "/user/tokens/verify", + f"/accounts/{ACCOUNT}/cfd_tunnel/{TUNNEL}/configurations", + "/zones?name=exposedgays.com", +]: + req = urllib.request.Request( + f"https://api.cloudflare.com/client/v4{path}", + headers={"Authorization": f"Bearer {tok}", "Content-Type": "application/json"}, + ) + try: + with urllib.request.urlopen(req, timeout=30) as r: + data = json.load(r) + print(path, "OK", data.get("success"), "errors=", data.get("errors")) + except urllib.error.HTTPError as e: + err = json.loads(e.read().decode()) + print(path, e.code, err.get("errors")) \ No newline at end of file diff --git a/deploy/try-cf-api-accounts.py b/deploy/try-cf-api-accounts.py new file mode 100644 index 0000000..5c455a4 --- /dev/null +++ b/deploy/try-cf-api-accounts.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +import json, re, urllib.request, urllib.error, paramiko +PASS = "Bbt9115xty9176!" +ACCOUNT = "2599c23bbb1255dbb73e8d34b4115fda" +TUNNEL = "03079a26-f14c-4622-b463-ba54a24f7472" +CF = "https://api.cloudflare.com/client/v4" + +c = paramiko.SSHClient(); c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +c.connect("10.10.0.1", username="admin", password=PASS, timeout=15) +_, o, e = c.exec_command("cat /usr/local/etc/wan-dns-failover.env", timeout=30) +o.channel.recv_exit_status() +TOK = re.search(r"CLOUDFLARE_API_TOKEN=([^\s\"']+)", (o.read()+e.read()).decode()).group(1) +c.close() + +def call(path, method="GET", body=None): + req = urllib.request.Request(CF+path, data=json.dumps(body).encode() if body else None, method=method, + headers={"Authorization": f"Bearer {TOK}", "Content-Type": "application/json"}) + try: + with urllib.request.urlopen(req, timeout=30) as r: return r.status, json.load(r) + except urllib.error.HTTPError as e: return e.code, json.loads(e.read().decode()) + +for path in ["/accounts", f"/accounts/{ACCOUNT}/cfd_tunnel", f"/accounts/{ACCOUNT}/cfd_tunnel/{TUNNEL}/configurations"]: + code, res = call(path) + print(path, code, "success=", res.get("success"), "errors=", res.get("errors")) + if res.get("result") and path.endswith("configurations"): + ing = res["result"].get("config", {}).get("ingress", []) + print(" ingress count", len(ing)) + print(" exposedgays", [i.get("hostname") for i in ing if i.get("hostname") and "exposed" in i.get("hostname","")]) \ No newline at end of file diff --git a/deploy/try-cf-graphql.py b/deploy/try-cf-graphql.py new file mode 100644 index 0000000..b53eeb4 --- /dev/null +++ b/deploy/try-cf-graphql.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 +import json, re, urllib.request, paramiko +PASS = "Bbt9115xty9176!" +ACCOUNT = "2599c23bbb1255dbb73e8d34b4115fda" +TUNNEL = "03079a26-f14c-4622-b463-ba54a24f7472" + +c = paramiko.SSHClient(); c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +c.connect("10.10.0.1", username="admin", password=PASS, timeout=15) +_, o, e = c.exec_command("cat /usr/local/etc/wan-dns-failover.env", timeout=30) +o.channel.recv_exit_status() +TOK = re.search(r"CLOUDFLARE_API_TOKEN=([^\s\"']+)", (o.read()+e.read()).decode()).group(1) +c.close() + +query = """ +query { + viewer { + accounts(filter: {accountTag: "%s"}) { + tunnels(filter: {uuid: "%s"}) { + id + name + config { + ingress { + hostname + service + } + } + } + } + } +} +""" % (ACCOUNT, TUNNEL) + +req = urllib.request.Request( + "https://api.cloudflare.com/client/v4/graphql", + data=json.dumps({"query": query}).encode(), + method="POST", + headers={"Authorization": f"Bearer {TOK}", "Content-Type": "application/json"}, +) +with urllib.request.urlopen(req, timeout=30) as r: + print(json.dumps(json.load(r), indent=2)[:5000]) \ No newline at end of file diff --git a/deploy/try-cloudflared-cli.py b/deploy/try-cloudflared-cli.py new file mode 100644 index 0000000..6dd0eb1 --- /dev/null +++ b/deploy/try-cloudflared-cli.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python3 +import paramiko +PASS = "Bbt9115xty9176!" +TUNNEL = "03079a26-f14c-4622-b463-ba54a24f7472" +CREDS = f"/etc/cloudflared/{TUNNEL}.json" +c = paramiko.SSHClient() +c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +c.connect("10.10.0.10", username="localadministrator", password=PASS, timeout=30) +cmds = [ + "cloudflared tunnel ingress --help 2>&1 | head -30", + f"echo '{PASS}' | sudo -S cloudflared tunnel --cred-file {CREDS} ingress validate /etc/cloudflared/config.yml 2>&1", + f"echo '{PASS}' | sudo -S cloudflared tunnel --cred-file {CREDS} ingress rule list 2>&1 | head -20", + "cloudflared tunnel --help 2>&1 | grep -i 'local\\|remote\\|config' | head -15", +] +for cmd in cmds: + print(f"\n=== {cmd[:85]} ===") + _, o, e = c.exec_command(cmd, timeout=60) + o.channel.recv_exit_status() + print((o.read()+e.read()).decode(errors="replace")[:3000]) +c.close() \ No newline at end of file diff --git a/deploy/try-route-dns.py b/deploy/try-route-dns.py new file mode 100644 index 0000000..95ef455 --- /dev/null +++ b/deploy/try-route-dns.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python3 +import paramiko + +PASS = "Bbt9115xty9176!" +TUNNEL = "03079a26-f14c-4622-b463-ba54a24f7472" +CREDS = f"/etc/cloudflared/{TUNNEL}.json" + +c = paramiko.SSHClient() +c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +c.connect("10.10.0.10", username="localadministrator", password=PASS, timeout=30) + +cmds = [ + f"echo '{PASS}' | sudo -S cloudflared tunnel --cred-file {CREDS} route dns --help 2>&1 | head -25", + f"echo '{PASS}' | sudo -S cloudflared tunnel --cred-file {CREDS} route dns {TUNNEL} www.exposedgays.com 2>&1", + f"echo '{PASS}' | sudo -S cloudflared tunnel --cred-file {CREDS} route dns {TUNNEL} exposedgays.com 2>&1", + "dig @1.1.1.1 www.exposedgays.com CNAME +short", + "curl -sSI --max-time 15 https://www.exposedgays.com/ | head -10", +] +for cmd in cmds: + print(f"\n=== {cmd[:90]} ===") + _, o, e = c.exec_command(cmd, timeout=60) + o.channel.recv_exit_status() + print((o.read() + e.read()).decode(errors="replace")[:3000]) +c.close() \ No newline at end of file diff --git a/deploy/try-tunnel-token-api.py b/deploy/try-tunnel-token-api.py new file mode 100644 index 0000000..bff03b1 --- /dev/null +++ b/deploy/try-tunnel-token-api.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +import json, re, urllib.request, urllib.error, paramiko +PASS = "Bbt9115xty9176!" +ACCOUNT = "2599c23bbb1255dbb73e8d34b4115fda" +TUNNEL = "03079a26-f14c-4622-b463-ba54a24f7472" +CF = "https://api.cloudflare.com/client/v4" + +c = paramiko.SSHClient(); c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +c.connect("10.10.0.1", username="admin", password=PASS, timeout=15) +_, o, e = c.exec_command("cat /usr/local/etc/wan-dns-failover.env", timeout=30) +o.channel.recv_exit_status() +TOK = re.search(r"CLOUDFLARE_API_TOKEN=([^\s\"']+)", (o.read()+e.read()).decode()).group(1) +c.close() + +endpoints = [ + f"/accounts/{ACCOUNT}/cfd_tunnel/{TUNNEL}/token", + f"/accounts/{ACCOUNT}/cfd_tunnel/{TUNNEL}/connections", + f"/accounts/{ACCOUNT}/cfd_tunnel/{TUNNEL}/routes?per_page=50", + f"/accounts/{ACCOUNT}/teamnet/routes?per_page=50", + f"/accounts/{ACCOUNT}/teamnet/virtual_networks", +] +for path in endpoints: + req = urllib.request.Request(CF+path, headers={"Authorization": f"Bearer {TOK}", "Content-Type": "application/json"}) + try: + with urllib.request.urlopen(req, timeout=30) as r: + data = json.load(r) + print(path, "OK", json.dumps(data)[:400]) + except urllib.error.HTTPError as e: + body = e.read().decode() + print(path, e.code, body[:200]) \ No newline at end of file diff --git a/deploy/verify-ns.py b/deploy/verify-ns.py new file mode 100644 index 0000000..1efcb0b --- /dev/null +++ b/deploy/verify-ns.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +import paramiko + +PASS = "Bbt9115xty9176!" +c = paramiko.SSHClient() +c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +c.connect("10.10.0.10", username="localadministrator", password=PASS, timeout=30) +for cmd in [ + "dig @1.1.1.1 exposedgays.com NS +short", + "dig @8.8.8.8 exposedgays.com NS +short", + "dig @ns71.domaincontrol.com exposedgays.com NS +short 2>&1 | head -5", +]: + print(f"$ {cmd}") + _, o, e = c.exec_command(cmd, timeout=30) + o.channel.recv_exit_status() + print((o.read() + e.read()).decode()) + print() +c.close() \ No newline at end of file diff --git a/deploy/verify.py b/deploy/verify.py new file mode 100644 index 0000000..c07ae11 --- /dev/null +++ b/deploy/verify.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +import paramiko + +c = paramiko.SSHClient() +c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +c.connect("10.10.0.10", username="localadministrator", password="Bbt9115xty9176!", timeout=20) + +cmds = [ + "curl -sS -o /dev/null -w '%{http_code}' -k -H 'Host: exposedgays.com' https://127.0.0.1/", + "curl -sS -o /dev/null -w '%{http_code}' http://10.10.0.115:3000/ 2>/dev/null || echo no_direct", + "ssh -o StrictHostKeyChecking=no localadministrator@10.10.0.115 'docker inspect ira4bw4nhbvm87o7s3fgqu6v-214624968606 --format \"{{json .Config.Labels}}\"' 2>&1 | head -c 2000", + "docker exec coolify-db psql -U coolify -d coolify -t -A -c \"SELECT fqdn,status FROM applications WHERE uuid='ira4bw4nhbvm87o7s3fgqu6v';\"", +] + +for cmd in cmds: + print("$", cmd[:120]) + _, o, e = c.exec_command(cmd, timeout=30) + print(o.read().decode()[:2500]) + print() + +c.close() \ No newline at end of file diff --git a/public/logo.svg b/public/logo.svg new file mode 100644 index 0000000..115a1c1 --- /dev/null +++ b/public/logo.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + EG + + + + + \ No newline at end of file diff --git a/public/manifest.json b/public/manifest.json index 07b81fd..8983420 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -6,17 +6,19 @@ "display": "standalone", "background_color": "#0d0a0f", "theme_color": "#ff2d6b", - "orientation": "portrait-primary", + "orientation": "any", "icons": [ { - "src": "/icon-192.png", - "sizes": "192x192", - "type": "image/png" + "src": "/logo.svg", + "sizes": "any", + "type": "image/svg+xml", + "purpose": "any" }, { - "src": "/icon-512.png", + "src": "/logo.svg", "sizes": "512x512", - "type": "image/png" + "type": "image/svg+xml", + "purpose": "maskable" } ] } \ No newline at end of file diff --git a/src/app/chat/page.tsx b/src/app/chat/page.tsx index b9c7b40..b475227 100644 --- a/src/app/chat/page.tsx +++ b/src/app/chat/page.tsx @@ -9,7 +9,7 @@ function ChatContent() { const userId = searchParams.get("user"); return ( -
+
); diff --git a/src/app/globals.css b/src/app/globals.css index aa05237..fb4cb3d 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -92,5 +92,41 @@ /* PWA safe areas */ .safe-bottom { - padding-bottom: env(safe-area-inset-bottom, 0); + padding-bottom: max(env(safe-area-inset-bottom, 0px), 4px); +} +.safe-top { + padding-top: env(safe-area-inset-top, 0); +} + +/* ID verification blur — nude photos in restricted states */ +.id-blur img, +.id-blur .explicit-content { + filter: blur(28px) !important; +} +.id-blur:hover img, +.id-blur:hover .explicit-content { + filter: blur(20px) !important; +} + +/* Mobile touch targets & scroll */ +.touch-manipulation { + touch-action: manipulation; +} +@supports (height: 100dvh) { + .min-h-screen { + min-height: 100dvh; + } +} + +/* Prevent iOS zoom on input focus */ +@media (max-width: 767px) { + input, select, textarea { + font-size: 16px !important; + } +} + +/* Overscroll containment for chat */ +.overscroll-contain { + overscroll-behavior: contain; + -webkit-overflow-scrolling: touch; } \ No newline at end of file diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 82d2b5f..a631eba 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -10,6 +10,10 @@ export const metadata: Metadata = { description: "100% free gay cruising map, chat, spots directory, and sex shop. No subscriptions ever.", manifest: "/manifest.json", + icons: { + icon: "/logo.svg", + apple: "/logo.svg", + }, appleWebApp: { capable: true, statusBarStyle: "black-translucent", @@ -21,7 +25,7 @@ export const viewport: Viewport = { themeColor: "#ff2d6b", width: "device-width", initialScale: 1, - maximumScale: 1, + viewportFit: "cover", }; export default function RootLayout({ diff --git a/src/app/page.tsx b/src/app/page.tsx index 54613da..d9d413c 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -8,7 +8,7 @@ export default function HomePage() { const vanillaMode = useVanillaMode(); return ( -
+
([]); const [onPrep, setOnPrep] = useState(false); const [stiStatus, setStiStatus] = useState(""); + const [state, setState] = useState(""); const [saving, setSaving] = useState(false); const [message, setMessage] = useState(""); @@ -36,8 +50,10 @@ export default function ProfilePage() { setKinks(profile.kinks || []); setOnPrep(profile.on_prep); setStiStatus(profile.sti_status || ""); + setState(profile.state || getUserState() || ""); } else { const local = localStorage.getItem("eg_profile"); + setState(getUserState() || ""); if (local) { try { const p = JSON.parse(local); @@ -66,6 +82,8 @@ export default function ProfilePage() { setSaving(true); setMessage(""); + if (state) setUserState(state); + const data = { display_name: isAnonymous ? null : displayName || null, age: parseInt(age) || null, @@ -75,6 +93,7 @@ export default function ProfilePage() { on_prep: onPrep, sti_status: stiStatus || null, is_anonymous: isAnonymous, + state: state || null, status: "online" as const, city: process.env.NEXT_PUBLIC_DEFAULT_CITY || "Fort Lauderdale", lat: parseFloat(process.env.NEXT_PUBLIC_DEFAULT_LAT || "26.1224"), @@ -103,6 +122,9 @@ export default function ProfilePage() { setSaving(false); }; + const idRequired = requiresIdVerification(state); + const verified = isIdVerified(profile?.id_verified); + if (loading) { return (
@@ -112,23 +134,51 @@ export default function ProfilePage() { } return ( -
-
-
-

Your Profile

-

+

+
+
+

Your Profile

+

{user ? `Signed in as ${user.email || "anonymous user"}` : "Browsing locally — sign up free for cloud sync"}

{!user && ( - )}
+ {idRequired && ( + + +
+ +
+

+ {verified ? "ID Verified" : "ID Verification Required"} +

+

+ {stateName(state)} law requires verification to view nude photos. +

+ {!verified && ( + + )} +
+
+
+
+ )} + Visibility @@ -145,13 +195,37 @@ export default function ProfilePage() { value={displayName} onChange={(e) => setDisplayName(e.target.value)} placeholder="Your name" - className="mt-1" + className="mt-1 h-11" />
)} + + + + + Location + + + + + + + + Details @@ -166,7 +240,7 @@ export default function ProfilePage() { placeholder="18+" min={18} max={99} - className="mt-1" + className="mt-1 h-11" />
@@ -177,7 +251,7 @@ export default function ProfilePage() { setPosition(position === pos ? null : pos)} > {pos} @@ -192,7 +266,7 @@ export default function ProfilePage() { value={bio} onChange={(e) => setBio(e.target.value)} placeholder="Say something..." - className="mt-1" + className="mt-1 h-11" />
@@ -203,7 +277,7 @@ export default function ProfilePage() { toggleKink(k)} > {k} @@ -223,12 +297,16 @@ export default function ProfilePage() { value={stiStatus} onChange={(e) => setStiStatus(e.target.value)} placeholder="e.g. Negative, tested Jan 2026" - className="mt-1" + className="mt-1 h-11" />
+ setIdModalOpen(true)} /> + + + {message && (

{message} @@ -237,7 +315,7 @@ export default function ProfilePage() { )} setAuthOpen(false)} /> + setIdModalOpen(false)} + onVerified={() => setIdModalOpen(false)} + />

); } \ No newline at end of file diff --git a/src/components/AgeGate.tsx b/src/components/AgeGate.tsx index 7375008..7a52529 100644 --- a/src/components/AgeGate.tsx +++ b/src/components/AgeGate.tsx @@ -3,7 +3,8 @@ import { useState } from "react"; import { Button } from "@/components/ui/button"; import { Label } from "@/components/ui/label"; -import { ShieldAlert } from "lucide-react"; +import { Logo } from "@/components/Logo"; +import { US_STATES, setUserState, requiresIdVerification } from "@/lib/id-verification"; interface AgeGateProps { onVerified: () => void; @@ -13,6 +14,7 @@ export function AgeGate({ onVerified }: AgeGateProps) { const [month, setMonth] = useState(""); const [day, setDay] = useState(""); const [year, setYear] = useState(""); + const [state, setState] = useState(""); const [error, setError] = useState(""); const [agreed, setAgreed] = useState(false); @@ -26,6 +28,11 @@ export function AgeGate({ onVerified }: AgeGateProps) { return; } + if (!state) { + setError("Please select your state — required for content access rules."); + return; + } + const dob = new Date(y, m - 1, d); const today = new Date(); let age = today.getFullYear() - dob.getFullYear(); @@ -46,15 +53,25 @@ export function AgeGate({ onVerified }: AgeGateProps) { localStorage.setItem("eg_age_verified", "true"); localStorage.setItem("eg_age_verified_at", new Date().toISOString()); + setUserState(state); + + if (requiresIdVerification(state)) { + localStorage.setItem("eg_id_verification_required", "true"); + } else { + localStorage.removeItem("eg_id_verification_required"); + } + onVerified(); }; + const idRequired = requiresIdVerification(state); + return ( -
-
+
+
-
- +
+

ExposedGays @@ -71,7 +88,7 @@ export function AgeGate({ onVerified }: AgeGateProps) { setDay(e.target.value)} - className="h-10 rounded-md border border-input bg-background px-2 text-sm" + className="h-11 rounded-md border border-input bg-background px-2 text-sm touch-manipulation" > {Array.from({ length: 31 }, (_, i) => ( @@ -95,7 +112,7 @@ export function AgeGate({ onVerified }: AgeGateProps) { setState(e.target.value)} + className="h-11 w-full rounded-md border border-input bg-background px-3 text-sm touch-manipulation" + > + + {US_STATES.map((s) => ( + + ))} + + {idRequired && ( +

+ {US_STATES.find((s) => s.code === state)?.name} requires ID verification + to view nude photos. You can verify after entering. +

+ )} +

+ @@ -136,7 +175,12 @@ export function AgeGate({ onVerified }: AgeGateProps) {

{error}

)} - diff --git a/src/components/CannedMessagesPanel.tsx b/src/components/CannedMessagesPanel.tsx new file mode 100644 index 0000000..fff1709 --- /dev/null +++ b/src/components/CannedMessagesPanel.tsx @@ -0,0 +1,216 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { MessageSquarePlus, Trash2, Pencil, Check, X } from "lucide-react"; +import { + loadCannedMessages, + saveCannedMessage, + deleteCannedMessage, + type CannedMessage, +} from "@/lib/canned-messages-store"; +import { MAX_CANNED_MESSAGES, MAX_CANNED_MESSAGE_LENGTH } from "@/lib/limits"; +import { useAuth } from "@/lib/auth/context"; + +interface CannedMessagesPanelProps { + compact?: boolean; + onSelect?: (content: string) => void; +} + +export function CannedMessagesPanel({ compact, onSelect }: CannedMessagesPanelProps) { + const { user } = useAuth(); + const [messages, setMessages] = useState([]); + const [editing, setEditing] = useState(null); + const [editTitle, setEditTitle] = useState(""); + const [editContent, setEditContent] = useState(""); + const [showNew, setShowNew] = useState(false); + const [newTitle, setNewTitle] = useState(""); + const [newContent, setNewContent] = useState(""); + const [error, setError] = useState(""); + + const refresh = async () => { + setMessages(await loadCannedMessages(user?.id)); + }; + + useEffect(() => { + refresh(); + }, [user?.id]); + + const startEdit = (msg: CannedMessage) => { + setEditing(msg.id); + setEditTitle(msg.title); + setEditContent(msg.content); + }; + + const saveEdit = async () => { + const { error: err } = await saveCannedMessage( + { id: editing!, title: editTitle, content: editContent }, + user?.id + ); + if (err) setError(err); + else { + setEditing(null); + setError(""); + await refresh(); + } + }; + + const handleCreate = async () => { + const { error: err } = await saveCannedMessage( + { title: newTitle, content: newContent }, + user?.id + ); + if (err) setError(err); + else { + setShowNew(false); + setNewTitle(""); + setNewContent(""); + setError(""); + await refresh(); + } + }; + + const handleDelete = async (id: string) => { + await deleteCannedMessage(id, user?.id); + await refresh(); + }; + + if (compact) { + return ( +
+ {messages.map((msg) => ( + + ))} + {messages.length === 0 && ( +

No quick replies — add them in Profile

+ )} +
+ ); + } + + return ( + + +
+ + + Quick Replies + + + {messages.length}/{MAX_CANNED_MESSAGES} + +
+

+ Save addresses, directions, hosting notes — tap to insert while messaging. +

+
+ + {messages.map((msg) => ( +
+ {editing === msg.id ? ( + <> + setEditTitle(e.target.value)} + placeholder="Label (e.g. Address)" + /> +