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:
206
frida/okhttp_hook.js
Normal file
206
frida/okhttp_hook.js
Normal 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
102
frida/run.sh
Executable 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
132
frida/server.py
Normal 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
101
frida/simple_hook.js
Normal 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.");
|
||||
});
|
||||
Reference in New Issue
Block a user