Search: FlightAware backbone, blob catalog, diagnostic infra

route-explorer's /api/token sits behind invisible Cloudflare Turnstile
that requires Apple's Private Access Token attestation. Third-party
iOS apps don't qualify for PAT issuance, and Linux Docker containers
can't pass it either (cross-OS fingerprint, even with patchright /
Camoufox). Migrates direct-flight search to FlightAware; multi-stop
and where-can-I-go remain via embedded SFSafariViewController.

- FlightAwareScheduleClient — scrapes route.rvt + trackpoll JSON for
  real schedules without auth. T+0..2 day window. Tests against
  captured HTML fixtures.
- BlobRouteClient — pulls the public Vercel blob route catalog
  route-explorer's frontend reads (no auth, no Turnstile).
- DiagnosticLogger + LoggingURLSessionDelegate + DiagnosticsView —
  device-shareable forensic trace. Boot header captures device, OS,
  locale, UA; share-sheet export of session logs.
- TurnstileDebugView — live WKWebView gate inspector. Used to prove
  the PAT-entitlement gap on a real device.
- RouteExplorerBrowserView — SFSafariViewController wrapper. Real
  Safari clears Turnstile naturally; the in-app browser opens at
  pre-filled search URLs. Surfaced from Search ("Open in
  route-explorer") and Settings → Tools.
- RouteExplorerTokenStore + RouteExplorerSetupView — bookmarklet
  capture flow (token round-tripped via flights://routeexplorer-token
  URL scheme). Kept dormant for future use.

backend/ — Docker proxy attempts (Playwright, patchright, Camoufox).
All fail on Linux because Cloudflare auto-denies before the Turnstile
widget renders. Documented; kept as scaffolding for a future paid-
solver integration.

scripts/probe_flightaware.py — reference algorithm for the FA path.
scripts/probe_nodriver.py — local-Mac sanity check confirming the
gate clears with real macOS Chrome (proves the blocker is
fingerprint-level, not network-level).
This commit is contained in:
Trey T
2026-06-06 01:09:59 -05:00
parent d122c95342
commit ba0688a412
70 changed files with 89096 additions and 209 deletions
+147
View File
@@ -0,0 +1,147 @@
#!/usr/bin/env python3
"""
Mint a rex_clearance + token via nodriver on this Mac, then verify
whether those credentials work:
A) from a plain curl on this Mac (same IP, no browser)
B) with an iOS Safari UA instead of Chrome UA
C) from a DIFFERENT IP (Anthropic infra via fly.io ipv6 / etc.)
Outputs the captured cookie + token so we can hardcode and replay.
"""
import asyncio, json, subprocess, sys
import nodriver as uc
BASE = "https://route-explorer.com"
SAFARI_UA = (
"Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) "
"AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 "
"Mobile/15E148 Safari/604.1"
)
async def mint() -> tuple[str, str, str]:
"""Returns (rex_clearance_value, am_user_session_value, token)."""
# Use nodriver's default Chrome stealth profile. Overriding UA at the
# process level breaks its detection-evasion shims. We test cross-UA
# replay separately after minting.
browser = await uc.start(headless=False)
tab = await browser.get(BASE + "/")
# accept cookies
await tab.evaluate("""
for (const b of document.querySelectorAll('button')) {
if (/accept|agree|allow/i.test((b.innerText||'').trim())) b.click();
}
""")
for tick in range(1, 60):
await asyncio.sleep(1)
status = await tab.evaluate("""
(async () => {
try { const r = await fetch('/api/token', { credentials: 'include' });
return r.status;
} catch (e) { return -1; }
})()
""", await_promise=True)
if status == 200:
print(f" cleared at t+{tick}s")
break
else:
browser.stop()
raise RuntimeError("Never cleared.")
body = await tab.evaluate("""
(async () => (await (await fetch('/api/token', {credentials:'include'})).text()))()
""", await_promise=True)
token = json.loads(body)["token"]
cookies = await browser.cookies.get_all()
rex = next((c for c in cookies if c.name == "rex_clearance"), None)
am = next((c for c in cookies if c.name == "am_user_session"), None)
if not rex:
browser.stop()
raise RuntimeError("Cleared but no rex_clearance cookie found.")
print(f"\n rex_clearance: {rex.value}")
print(f" am_user_session: {am.value if am else '<none>'}")
print(f" token: {token}")
print(f" cookie expires: {getattr(rex, 'expires', None)}")
browser.stop()
return rex.value, am.value if am else "", token
def curl(cookie_jar: str, ua: str, label: str) -> int:
"""Replay /api/token via curl with given cookies + UA, return HTTP status."""
cmd = [
"/usr/bin/curl", "-s", "-o", "/tmp/replay_body", "-w", "%{http_code}",
f"{BASE}/api/token",
"-H", f"User-Agent: {ua}",
"-H", "Accept: application/json",
"-H", f"Origin: {BASE}",
"-H", f"Referer: {BASE}/",
"-H", f"Cookie: {cookie_jar}",
]
r = subprocess.run(cmd, capture_output=True, text=True, timeout=15)
code = int(r.stdout.strip() or 0)
body = open("/tmp/replay_body").read()[:200]
print(f" {label}: HTTP {code} body: {body}")
return code
def main():
print("Minting credentials via nodriver…")
rex_val, am_val, token = uc.loop().run_until_complete(mint())
cookie_jar = f"rex_clearance={rex_val}; am_user_session={am_val}"
print("\n=== A: same Mac IP, iOS Safari UA, captured cookies ===")
curl(cookie_jar, SAFARI_UA, " same-IP/iOS-UA")
print("\n=== B: same Mac IP, Chrome UA (UA mismatch test) ===")
chrome_ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120 Safari/537.36"
curl(cookie_jar, chrome_ua, " same-IP/Chrome-UA")
print("\n=== C: flight-search with captured token ===")
cmd = [
"/usr/bin/curl", "-s", "-o", "/tmp/fs_body", "-w", "%{http_code}",
"-X", "POST", f"{BASE}/api/flight-search",
"-H", f"User-Agent: {SAFARI_UA}",
"-H", "Content-Type: application/json",
"-H", f"Origin: {BASE}",
"-H", f"Referer: {BASE}/",
"-H", f"Cookie: {cookie_jar}",
"-H", f"X-API-Token: {token}",
"-d", json.dumps({
"endpoint": "/route",
"body": {"json": {
"departureAirportIata": "DAL",
"arrivalAirportIata": "HOU",
"departureDates": ["2026-05-31"],
"maxStops": 0, "limit": 20, "includeAppendix": True,
}},
}),
]
r = subprocess.run(cmd, capture_output=True, text=True, timeout=15)
fs_code = int(r.stdout.strip() or 0)
body = open("/tmp/fs_body").read()
print(f" /api/flight-search: HTTP {fs_code}")
if fs_code == 200:
data = json.loads(body)
conns = data.get("json", {}).get("connections", [])
print(f"{len(conns)} connections")
for c in conns[:5]:
for f in c.get("flights", []):
print(f" {f['carrierIata']}{f['flightNumber']} "
f"{f['departure']['airportIata']}@{f['departure']['dateTime'][11:16]}"
f"{f['arrival']['airportIata']}@{f['arrival']['dateTime'][11:16]} "
f"({f.get('equipmentIata','?')})")
else:
print(f" body: {body[:300]}")
print(f"\n=== CAPTURED FOR HARDCODING ===")
print(f"REX_CLEARANCE = {rex_val!r}")
print(f"AM_USER_SESSION = {am_val!r}")
print(f"TOKEN = {token!r}")
if __name__ == "__main__":
main()