#!/usr/bin/env python3 """ tailscale-verify-portal.py: Tailscale verification portal for Human-in-the-Loop age verification. Serves on Tailscale IP (100.123.153.75:8765) and/or localhost. Endpoints: GET /status - JSON status of nodes awaiting verification GET /verify/ - Generates fresh Instagram OAuth linking URL from the node's browser and 302 redirects GET /check/ - Polls node to see if age gate has cleared into active chat session """ import http.server import json import os import re import socketserver import subprocess import sys import time import urllib.parse PORT = 8765 BIND_HOST = "0.0.0.0" NETVM_DIR = "/home/super/Projects/NetVM" def get_tailscale_ip(): try: out = subprocess.check_output(["tailscale", "ip", "-4"], text=True).strip().splitlines() for line in out: line = line.strip() if re.match(r"^\d+\.\d+\.\d+\.\d+$", line): return line except Exception: pass return "100.123.153.75" def get_tailscale_dns(): try: out = subprocess.check_output(["tailscale", "status", "--json"], text=True) data = json.loads(out) self_dns = data.get("Self", {}).get("DNSName") if self_dns: return self_dns.rstrip(".") except Exception: pass return "bl.tailfb5960.ts.net" def get_node_cdp_port(node): nodes_file = os.path.join(NETVM_DIR, "NODES.md") pinned = {"muse": 9410, "pip": 9420, "646": 9430, "opm": 9440, "def": 9450, "dev": 9455} if node in pinned: return pinned[node] try: with open(nodes_file) as f: for line in f: if not line.strip().startswith("|"): continue cells = [c.strip() for c in line.strip("|").split("|")] if len(cells) >= 4 and cells[0] == node and cells[3].isdigit(): return int(cells[3]) except Exception: pass return 9450 def fetch_node_ig_link(node): """Query CDP within node netns to get fresh Meta Accounts Center OAuth URL.""" port = get_node_cdp_port(node) script = f""" import json, websocket, sys, urllib.request try: tabs = json.loads(urllib.request.urlopen("http://127.0.0.1:{port}/json").read()) except Exception as e: print(json.dumps({{"error": f"CDP unreachable: {{e}}"}})) sys.exit(0) muse_tab = next((t for t in tabs if "muse.ai" in t.get("url", "") and t.get("type") == "page"), None) if not muse_tab: print(json.dumps({{"error": "No muse.ai tab open"}})) sys.exit(0) try: ws = websocket.create_connection(muse_tab["webSocketDebuggerUrl"], timeout=5) expr = \'\'\'(async()=>{{ try {{ const r = await fetch('/api/hatch/age-confirmation/linking-web-auth?account_type=instagram', {{ headers: {{'Accept': 'application/json'}} }}); return await r.json(); }} catch(e) {{ return {{error: String(e)}}; }} }})()\'\'\' ws.send(json.dumps({{"id": 1, "method": "Runtime.evaluate", "params": {{"expression": expr, "awaitPromise": True, "returnByValue": True}}}})) while True: msg = json.loads(ws.recv()) if msg.get("id") == 1: val = msg.get("result", {{}}).get("result", {{}}).get("value", {{}}) print(json.dumps(val)) break ws.close() except Exception as e: print(json.dumps({{"error": f"CDP evaluate failed: {{e}}"}})) """ cmd = ["sudo", "-n", "ip", "netns", "exec", f"warp-{node}", sys.executable, "-c", script] try: res = subprocess.run(cmd, capture_output=True, text=True, timeout=10) lines = res.stdout.strip().splitlines() if lines: data = json.loads(lines[-1]) return data except Exception as e: return {"error": str(e)} return {"error": "No response from node"} def check_node_cleared(node): """Check if node has transitioned past access/verification into active chat.""" checker = os.path.join(NETVM_DIR, "bin", "accounts-health.py") port = get_node_cdp_port(node) cmd = ["sudo", "-n", "ip", "netns", "exec", f"warp-{node}", sys.executable, checker, str(port)] try: res = subprocess.run(cmd, capture_output=True, text=True, timeout=10) lines = res.stdout.strip().splitlines() if lines: data = json.loads(lines[-1]) return data except Exception as e: return {"error": str(e)} return {"error": "No response from checker"} class VerifyHandler(http.server.BaseHTTPRequestHandler): def do_GET(self): parsed = urllib.parse.urlparse(self.path) parts = [p for p in parsed.path.split("/") if p] if not parts or parts[0] == "": self.send_response(200) self.send_header("Content-Type", "text/html; charset=utf-8") self.end_headers() html = """ NetVM Operator Portal

NetVM Client Verification Portal

Nodes pending verification:

Tap to Link Instagram for Node 'def'

""" self.wfile.write(html.encode("utf-8")) return if parts[0] == "verify" and len(parts) >= 2: node = parts[1] data = fetch_node_ig_link(node) url = data.get("url") if url: # 302 redirect directly to Instagram OAuth login self.send_response(302) self.send_header("Location", url) self.end_headers() return else: self.send_response(500) self.send_header("Content-Type", "application/json") self.end_headers() self.wfile.write(json.dumps({"error": "Failed to get Instagram link", "detail": data}).encode("utf-8")) return if parts[0] == "check" and len(parts) >= 2: node = parts[1] st = check_node_cleared(node) self.send_response(200) self.send_header("Content-Type", "application/json") self.end_headers() self.wfile.write(json.dumps(st, indent=2).encode("utf-8")) return if parts[0] == "status": self.send_response(200) self.send_header("Content-Type", "application/json") self.end_headers() self.wfile.write(json.dumps({"portal": "online", "ts_ip": get_tailscale_ip(), "ts_dns": get_tailscale_dns()}).encode("utf-8")) return self.send_response(404) self.end_headers() def log_message(self, format, *args): # Concise logging sys.stderr.write(f"[PORTAL] {self.address_string()} - {format % args}\n") def run(): with socketserver.TCPServer((BIND_HOST, PORT), VerifyHandler) as httpd: print(f"Tailscale Verification Portal serving on http://{get_tailscale_ip()}:{PORT}/") print(f"Tailscale DNS: http://{get_tailscale_dns()}:{PORT}/") sys.stdout.flush() httpd.serve_forever() if __name__ == "__main__": run()