Initial commit: Flights iOS app

Flight search app built on FlightConnections.com API data.
Features: airport search with autocomplete, browse by country/state/map,
flight schedules by route and date, multi-airline support with per-airline
schedule loading. Includes 4,561-airport GPS database for map browsing.
Adaptive light/dark mode UI inspired by Flighty.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-04-08 15:01:07 -05:00
commit 3790792040
46 changed files with 5116 additions and 0 deletions

206
frida/okhttp_hook.js Normal file
View File

@@ -0,0 +1,206 @@
/*
* Universal OkHttp Response Interceptor
* Hooks OkHttp's response chain to capture ALL HTTP responses.
* Works with Spirit, Delta, United (all use OkHttp/Retrofit).
*
* Sends captured data to a local server via Frida's send().
*/
Java.perform(function() {
console.log("[*] OkHttp Response Interceptor starting...");
// Filter: only capture responses from these domains
var targetDomains = [
"api.spirit.com",
"www.delta.com",
"mobileapi.united.com",
"content.spirit.com",
"content.delta.com"
];
// Filter: only capture these API paths
var targetPaths = [
// Spirit
"/customermobileprod/",
"/v1/getboastatus",
"/v1/getboaparameters",
"/v2/Token",
"/v3/mytrips",
"/v1/booking",
"/v3/GetFlightInfoBI",
"/v5/Flight/Search",
// Delta
"/api/mobile/asl",
"/api/mobile/getFlightStatus",
"/api/mobile/getFlightStatusByLeg",
"/api/mobile/login",
"/api/mobile/getDashboard",
"/api/mobile/getUpgradeEligibilityInfo",
// United
"/standbylistservice/",
"/upgradelistservice/",
"/flightstatusservice/",
"/checkinservice/",
"/passriderlistservice/"
];
function shouldCapture(url) {
var domainMatch = false;
var pathMatch = false;
for (var i = 0; i < targetDomains.length; i++) {
if (url.indexOf(targetDomains[i]) !== -1) {
domainMatch = true;
break;
}
}
if (!domainMatch) return false;
for (var j = 0; j < targetPaths.length; j++) {
if (url.indexOf(targetPaths[j]) !== -1) {
pathMatch = true;
break;
}
}
return pathMatch;
}
// === Hook OkHttp3 RealCall.getResponseWithInterceptorChain ===
try {
var OkHttpClient = Java.use("okhttp3.OkHttpClient");
var Request = Java.use("okhttp3.Request");
var Response = Java.use("okhttp3.Response");
var ResponseBody = Java.use("okhttp3.ResponseBody");
var BufferClass = Java.use("okio.Buffer");
var MediaType = Java.use("okhttp3.MediaType");
// Hook the Interceptor.Chain.proceed to capture request+response
var RealInterceptorChain = Java.use("okhttp3.internal.http.RealInterceptorChain");
RealInterceptorChain.proceed.overload("okhttp3.Request").implementation = function(request) {
var url = request.url().toString();
var response = this.proceed(request);
if (shouldCapture(url)) {
try {
var method = request.method();
var reqBody = null;
// Capture request body
if (request.body() !== null) {
var reqBuffer = BufferClass.$new();
request.body().writeTo(reqBuffer);
reqBody = reqBuffer.readUtf8();
}
// Capture request headers
var reqHeaders = {};
var headerNames = request.headers();
for (var i = 0; i < headerNames.size(); i++) {
reqHeaders[headerNames.name(i)] = headerNames.value(i);
}
// Capture response
var statusCode = response.code();
var respBody = null;
var respHeaders = {};
// Response headers
var respHeaderObj = response.headers();
for (var j = 0; j < respHeaderObj.size(); j++) {
respHeaders[respHeaderObj.name(j)] = respHeaderObj.value(j);
}
// Response body (need to peek without consuming)
var body = response.body();
if (body !== null) {
var source = body.source();
source.request(Long.MAX_VALUE);
var buffer = source.getBuffer().clone();
respBody = buffer.readUtf8();
}
var captured = {
type: "HTTP_RESPONSE",
timestamp: new Date().toISOString(),
method: method,
url: url,
status: statusCode,
requestHeaders: reqHeaders,
requestBody: reqBody,
responseHeaders: respHeaders,
responseBody: respBody
};
// Send to Frida host
send(captured);
console.log("[+] CAPTURED: " + method + " " + url + " -> " + statusCode + " (" + (respBody ? respBody.length : 0) + " chars)");
} catch(e) {
console.log("[-] Capture error for " + url + ": " + e);
}
}
return response;
};
console.log("[+] OkHttp RealInterceptorChain.proceed hooked");
} catch(e) {
console.log("[-] OkHttp3 hook failed: " + e);
console.log("[*] Trying alternative hook...");
// Alternative: Hook at a higher level
try {
var Interceptor = Java.use("okhttp3.Interceptor");
// This approach hooks via adding our own interceptor
console.log("[*] Alternative approach needed - see app-specific hooks");
} catch(e2) {
console.log("[-] Alternative also failed: " + e2);
}
}
// === Also hook Retrofit response callbacks ===
try {
var CallbackClass = Java.use("retrofit2.OkHttpCall");
CallbackClass.parseResponse.implementation = function(rawResponse) {
var response = this.parseResponse(rawResponse);
try {
var url = rawResponse.request().url().toString();
if (shouldCapture(url)) {
var body = response.body();
if (body !== null) {
console.log("[+] Retrofit response: " + url);
console.log("[+] Body class: " + body.getClass().getName());
console.log("[+] Body: " + body.toString().substring(0, Math.min(500, body.toString().length)));
send({
type: "RETROFIT_RESPONSE",
timestamp: new Date().toISOString(),
url: url,
bodyClass: body.getClass().getName(),
body: body.toString()
});
}
}
} catch(e) {
// Ignore parse errors
}
return response;
};
console.log("[+] Retrofit parseResponse hooked");
} catch(e) {
console.log("[-] Retrofit hook failed: " + e);
}
// === Java Long for buffer request ===
var Long = Java.use("java.lang.Long");
Long.MAX_VALUE.value;
console.log("[*] OkHttp Response Interceptor ready. Waiting for traffic...");
});

102
frida/run.sh Executable file
View File

@@ -0,0 +1,102 @@
#!/bin/bash
# Boot emulator, setup Frida, and run the capture server for airline apps.
# Usage: ./run.sh [spirit|delta|united|all]
set -e
EMU=/Users/treyt/Library/Android/sdk/emulator/emulator
ADB=/Users/treyt/Library/Android/sdk/platform-tools/adb
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
FRIDA_SERVER="/data/local/tmp/frida-server"
AIRLINE=${1:-spirit}
# Map airline to package name
case "$AIRLINE" in
spirit) PKG="com.spirit.customerapp" ;;
delta) PKG="com.delta.mobile.android" ;;
united) PKG="com.united.mobile.android" ;;
all) PKG="all" ;;
*) echo "Usage: $0 [spirit|delta|united|all]"; exit 1 ;;
esac
echo "========================================="
echo "Airline API Capture via Frida"
echo "Target: $AIRLINE ($PKG)"
echo "========================================="
# 1. Check if emulator is running
if ! $ADB devices 2>/dev/null | grep -q "emulator"; then
echo "[1/5] Booting emulator..."
$EMU -avd Pixel_6_API_28 -writable-system -no-snapshot-load -no-audio -gpu swiftshader_indirect &
sleep 30
$ADB wait-for-device
$ADB root
sleep 3
$ADB remount
else
echo "[1/5] Emulator already running"
fi
# 2. Start Frida server
echo "[2/5] Starting Frida server..."
$ADB root 2>/dev/null
sleep 1
if ! $ADB shell "ps -A | grep frida-server" 2>/dev/null | grep -q frida; then
if ! $ADB shell "test -f $FRIDA_SERVER" 2>/dev/null; then
echo " Pushing frida-server..."
$ADB push /tmp/frida-server $FRIDA_SERVER
$ADB shell chmod 755 $FRIDA_SERVER
fi
$ADB shell "$FRIDA_SERVER -D &"
sleep 3
fi
echo " Frida server running"
# 3. Verify Frida connection
echo "[3/5] Verifying Frida..."
frida-ps -U | head -3
echo " Connected"
# 4. Install app if needed
echo "[4/5] Checking app installation..."
if [ "$PKG" = "all" ]; then
for pkg in com.spirit.customerapp com.delta.mobile.android com.united.mobile.android; do
if ! $ADB shell pm list packages | grep -q "$pkg"; then
echo " $pkg not installed - install manually first"
else
echo " $pkg OK"
fi
done
else
if ! $ADB shell pm list packages | grep -q "$PKG"; then
echo " Installing $PKG..."
case "$AIRLINE" in
spirit) $ADB install ~/Desktop/code/flights/apps/com.spirit.customerapp*.apk ;;
delta) $ADB install-multiple /tmp/delta_apk/base.apk /tmp/delta_apk/split_config.arm64_v8a.apk /tmp/delta_apk/split_config.xxhdpi.apk ;;
united) $ADB install ~/Desktop/code/flights/apps/united-airlines.apk ;;
esac
else
echo " $PKG installed"
fi
fi
# 5. Launch app and start capture
echo "[5/5] Launching capture..."
if [ "$PKG" = "all" ]; then
echo "Run this script separately for each airline"
exit 0
fi
# Launch the app
$ADB shell monkey -p $PKG -c android.intent.category.LAUNCHER 1 2>/dev/null
sleep 5
echo ""
echo "========================================="
echo "Starting Frida capture server..."
echo "Interact with the $AIRLINE app to trigger API calls."
echo "========================================="
echo ""
python3 "$SCRIPT_DIR/server.py" "$PKG" "$SCRIPT_DIR/okhttp_hook.js"

132
frida/server.py Normal file
View File

@@ -0,0 +1,132 @@
#!/usr/bin/env python3
"""
Frida data receiver server.
Receives captured HTTP responses from the Frida hook and saves them.
Also provides a simple API to query captured data.
"""
import json
import frida
import sys
import os
import time
from pathlib import Path
from http.server import HTTPServer, BaseHTTPRequestHandler
import threading
CAPTURE_DIR = Path(__file__).parent / "captures"
CAPTURE_DIR.mkdir(exist_ok=True)
captured_data = []
def on_message(message, data):
"""Handle messages from Frida hook."""
if message["type"] == "send":
payload = message["payload"]
captured_data.append(payload)
url = payload.get("url", "unknown")
status = payload.get("status", "?")
method = payload.get("method", "?")
body_len = len(payload.get("responseBody", "") or "")
print(f"\n{'='*60}")
print(f"[CAPTURED] {method} {url}")
print(f" Status: {status} | Response: {body_len} chars")
# Save to file
ts = time.strftime("%Y%m%d_%H%M%S")
domain = url.split("/")[2] if "/" in url else "unknown"
filename = f"{ts}_{domain}_{method}_{status}.json"
filepath = CAPTURE_DIR / filename
with open(filepath, "w") as f:
json.dump(payload, f, indent=2)
print(f" Saved: {filepath.name}")
# Print response body preview
resp_body = payload.get("responseBody", "")
if resp_body:
try:
parsed = json.loads(resp_body)
print(f" Response preview: {json.dumps(parsed, indent=2)[:500]}")
except:
print(f" Response preview: {resp_body[:300]}")
elif message["type"] == "error":
print(f"[ERROR] {message['stack']}")
def attach_to_app(package_name, script_path):
"""Attach Frida to a running app and load the hook script."""
device = frida.get_usb_device()
print(f"[*] Connected to: {device.name}")
# Try to attach to running process first
try:
session = device.attach(package_name)
print(f"[*] Attached to running process: {package_name}")
except frida.ProcessNotFoundError:
# Spawn it
print(f"[*] Spawning: {package_name}")
pid = device.spawn([package_name])
session = device.attach(pid)
device.resume(pid)
print(f"[*] Spawned and attached: PID {pid}")
with open(script_path) as f:
script_code = f.read()
script = session.create_script(script_code)
script.on("message", on_message)
script.load()
print(f"[*] Script loaded. Intercepting traffic for {package_name}...")
return session
def main():
if len(sys.argv) < 2:
print("Usage: python server.py <package_name> [hook_script.js]")
print("")
print("Examples:")
print(" python server.py com.spirit.customerapp")
print(" python server.py com.delta.mobile.android")
print(" python server.py com.united.mobile.android")
print("")
print("Default hook script: okhttp_hook.js")
sys.exit(1)
package_name = sys.argv[1]
script_path = sys.argv[2] if len(sys.argv) > 2 else str(Path(__file__).parent / "okhttp_hook.js")
print(f"[*] Target: {package_name}")
print(f"[*] Script: {script_path}")
print(f"[*] Captures: {CAPTURE_DIR}")
print("")
session = attach_to_app(package_name, script_path)
print("")
print("[*] Ready. Interact with the app to trigger API calls.")
print("[*] Press Ctrl+C to stop.")
print("")
try:
sys.stdin.read()
except KeyboardInterrupt:
print("\n[*] Stopping...")
session.detach()
# Save all captured data
if captured_data:
summary_path = CAPTURE_DIR / f"all_captures_{time.strftime('%Y%m%d_%H%M%S')}.json"
with open(summary_path, "w") as f:
json.dump(captured_data, f, indent=2)
print(f"[*] Saved {len(captured_data)} captures to {summary_path}")
if __name__ == "__main__":
main()

101
frida/simple_hook.js Normal file
View File

@@ -0,0 +1,101 @@
/*
* Simple URL connection logger.
* Hooks at the lowest level to catch ALL HTTP traffic regardless of OkHttp version.
*/
Java.perform(function() {
console.log("[*] Simple HTTP hook starting...");
// Hook HttpURLConnection
try {
var HttpURLConnection = Java.use("java.net.HttpURLConnection");
HttpURLConnection.getInputStream.implementation = function() {
var url = this.getURL().toString();
var code = this.getResponseCode();
console.log("[HTTP] " + code + " " + url);
return this.getInputStream();
};
console.log("[+] HttpURLConnection.getInputStream hooked");
} catch(e) {
console.log("[-] HttpURLConnection hook failed: " + e);
}
// Hook OkHttp Response - try multiple class paths
var okHttpClasses = [
"okhttp3.internal.http.RealInterceptorChain",
"okhttp3.internal.connection.RealInterceptorChain",
"okhttp3.RealCall"
];
for (var i = 0; i < okHttpClasses.length; i++) {
try {
var cls = Java.use(okHttpClasses[i]);
var methods = cls.class.getDeclaredMethods();
console.log("[*] Found " + okHttpClasses[i] + " with " + methods.length + " methods:");
for (var j = 0; j < methods.length; j++) {
console.log(" " + methods[j].getName());
}
} catch(e) {
console.log("[-] " + okHttpClasses[i] + ": not found");
}
}
// Hook OkHttp Response.body() to see what responses come back
try {
var Response = Java.use("okhttp3.Response");
Response.body.implementation = function() {
var body = this.body();
try {
var url = this.request().url().toString();
var code = this.code();
console.log("[OkHttp] " + code + " " + url);
if (body !== null && url.indexOf("spirit.com") !== -1 ||
url.indexOf("delta.com") !== -1 || url.indexOf("united.com") !== -1) {
// Try to peek at the body
try {
var source = body.source();
source.request(java_lang_Long.MAX_VALUE.value);
var buffer = source.getBuffer().clone();
var bodyStr = buffer.readUtf8();
if (bodyStr.length > 0) {
console.log("[BODY] (" + bodyStr.length + " chars) " + bodyStr.substring(0, Math.min(2000, bodyStr.length)));
send({
type: "RESPONSE",
url: url,
status: code,
body: bodyStr
});
}
} catch(be) {
console.log("[BODY-ERR] " + be);
}
}
} catch(e) {
// ignore
}
return body;
};
console.log("[+] OkHttp Response.body() hooked");
} catch(e) {
console.log("[-] OkHttp Response hook failed: " + e);
}
var java_lang_Long = Java.use("java.lang.Long");
// Also hook URL.openConnection for non-OkHttp traffic
try {
var URL = Java.use("java.net.URL");
URL.openConnection.overload().implementation = function() {
console.log("[URL] " + this.toString());
return this.openConnection();
};
console.log("[+] URL.openConnection hooked");
} catch(e) {
console.log("[-] URL hook failed: " + e);
}
console.log("[*] Simple HTTP hook ready.");
});