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
+86
View File
@@ -0,0 +1,86 @@
#!/usr/bin/env python3
"""
nodriver-based probe — the modern Cloudflare-evading browser library.
If this can't mint a route-explorer.com token, no programmatic approach can.
"""
import asyncio, json
import nodriver as uc
BASE = "https://route-explorer.com"
async def main():
browser = await uc.start(headless=False) # headed = best chance
tab = await browser.get(BASE + "/")
print("loaded homepage")
# accept cookies
await tab.evaluate("""
for (const b of document.querySelectorAll('button')) {
if (/accept|agree|allow/i.test((b.innerText||'').trim())) b.click();
}
""")
print("accepted cookies (if banner present)")
cleared = False
for tick in range(1, 45):
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)
# also try the page's Retry button
await tab.evaluate("""
for (const b of document.querySelectorAll('button')) {
if (/retry/i.test((b.innerText||'').trim())) b.click();
}
""")
cookies = await browser.cookies.get_all()
cookie_names = sorted(c.name for c in cookies if "route-explorer" in (c.domain or "") or not c.domain)
print(f"t+{tick:2d}s /api/token→{status} cookies={cookie_names}")
if status == 200:
cleared = True
break
if cleared:
token_body = await tab.evaluate("""
(async () => {
const r = await fetch('/api/token', { credentials: 'include' });
return await r.text();
})()
""", await_promise=True)
print(f"TOKEN BODY: {token_body[:200]}")
# try flight-search
result = await tab.evaluate("""
(async () => {
const tk = JSON.parse(await (await fetch('/api/token', {credentials:'include'})).text()).token;
const r = await fetch('/api/flight-search', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json', 'X-API-Token': tk },
body: JSON.stringify({
endpoint: '/route',
body: { json: {
departureAirportIata: 'DAL',
arrivalAirportIata: 'HOU',
departureDates: [new Date().toISOString().substring(0,10)],
maxStops: 0, limit: 20, includeAppendix: true
}}
})
});
return JSON.stringify({status: r.status, body: (await r.text()).substring(0, 1000)});
})()
""", await_promise=True)
print(f"flight-search → {result}")
else:
print("NEVER CLEARED — nodriver also can't pass Turnstile.")
await asyncio.sleep(2)
browser.stop()
if __name__ == "__main__":
uc.loop().run_until_complete(main())