mirror of
https://github.com/xweiba/location-spoofer.git
synced 2026-09-21 22:30:46 +08:00
- iOS 虚拟定位工具,基于本地 HTTP 代理 MITM 方案 - MapKit 原生地图体验,支持搜索、收藏、实时定位 - 完整的设置引导流程(证书安装、WiFi 代理配置、环境验证) - 支持 iOS 15+,SwiftUI 构建
235 lines
10 KiB
Swift
235 lines
10 KiB
Swift
import Foundation
|
||
import SwiftUI
|
||
|
||
@MainActor
|
||
final class SetupCoordinator: ObservableObject {
|
||
@Published private(set) var trustState: CertificateTrustState = .checking
|
||
@Published var message = ""
|
||
@Published var isBrowsingWithoutTrust = false
|
||
@Published var testLog = ""
|
||
// 启动检测失败才弹引导页;检测通过则保持 false
|
||
@Published var needsSetup = false
|
||
|
||
let certificateStore = CertificateAuthorityStore()
|
||
let proxy = ProxyManager.shared
|
||
private var isVerificationRunning = false
|
||
|
||
init() { RuntimeLogger.info("APP", "Setup", "初始化") }
|
||
|
||
private var appVersion: String {
|
||
let v = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "?"
|
||
let b = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String ?? "?"
|
||
return "\(v) (\(b))"
|
||
}
|
||
|
||
var canModify: Bool { proxy.isRunning && trustState == .trusted }
|
||
|
||
func refreshTrust() async {
|
||
trustState = .checking
|
||
message = "正在检测…"
|
||
testLog = ""
|
||
do {
|
||
_ = try certificateStore.ensure()
|
||
if !proxy.isRunning { try await proxy.start() }
|
||
// 强制走代理 POST 到 Apple 定位接口:TLS 成功 = CA 信任
|
||
let config = URLSessionConfiguration.ephemeral
|
||
config.timeoutIntervalForRequest = 5
|
||
config.timeoutIntervalForResource = 8
|
||
config.connectionProxyDictionary = [
|
||
kCFNetworkProxiesHTTPEnable as String: true,
|
||
kCFNetworkProxiesHTTPProxy as String: "127.0.0.1",
|
||
kCFNetworkProxiesHTTPPort as String: 8888,
|
||
]
|
||
// 用当前保存的虚拟定位坐标做测试;没有则用默认坐标
|
||
let saved = WlocSettingsStore.load()
|
||
let testLat = saved?.latitude ?? 22.543099
|
||
let testLon = saved?.longitude ?? 113.934576
|
||
let testAccuracy = saved?.accuracy ?? 25
|
||
let req = makeWlocRequest()
|
||
let (_, resp) = try await URLSession(configuration: config).data(for: req)
|
||
let status = (resp as? HTTPURLResponse)?.statusCode ?? 0
|
||
// 400 = Apple 拒绝了测试请求体,但 TLS 握手成功 = 证书已信任
|
||
if status == 0 {
|
||
trustState = .unavailable
|
||
message = "代理链路异常,未收到响应"
|
||
return
|
||
}
|
||
// 走同一套改写验证:Go Core 内模拟 Apple 响应并确认坐标被改写
|
||
let patchResult = CoreBridge.testWlocPatch(lat: testLat, lon: testLon, accuracy: testAccuracy)
|
||
if patchResult.hasPrefix("ok:") {
|
||
trustState = .trusted
|
||
message = "✓ 定位环境正常(返回 \(status))"
|
||
} else {
|
||
trustState = .unavailable
|
||
message = "定位数据改写验证失败:\(patchResult)"
|
||
}
|
||
} catch {
|
||
trustState = .unavailable
|
||
let ns = error as NSError
|
||
if ns.domain == NSURLErrorDomain && ns.code == -1202 {
|
||
message = "CA 证书未信任,请去「设置→通用→关于→证书信任设置」开启或重新安装"
|
||
} else if ns.domain == NSURLErrorDomain && ns.code == -1001 {
|
||
message = "检测超时,请检查代理是否正常"
|
||
} else if ns.domain == NSURLErrorDomain && ns.code == -1200 {
|
||
message = "代理未启动或无法连接"
|
||
} else {
|
||
message = "检测失败 [\(ns.domain) \(ns.code)]: \(ns.localizedDescription)"
|
||
}
|
||
}
|
||
needsSetup = !canModify
|
||
}
|
||
|
||
func sceneDidBecomeActive() {}
|
||
func browseMapWithoutSetup() { isBrowsingWithoutTrust = true; needsSetup = false }
|
||
func completeSetup() { needsSetup = false }
|
||
func requestSetup() { needsSetup = true }
|
||
|
||
// MARK: - Step-by-step verification test
|
||
|
||
private func makeWlocRequest() -> URLRequest {
|
||
var req = URLRequest(url: URL(string: "https://gs-loc.apple.com/clls/wloc")!)
|
||
req.httpMethod = "POST"
|
||
req.httpBody = CoreBridge.testWlocRequestData()
|
||
req.setValue("application/x-protobuf", forHTTPHeaderField: "Content-Type")
|
||
req.setValue("wloc/1.0", forHTTPHeaderField: "User-Agent")
|
||
req.setValue("application/x-protobuf", forHTTPHeaderField: "Accept")
|
||
return req
|
||
}
|
||
|
||
func runVerificationTest(testLat: Double = 22.543099, testLon: Double = 113.934576) async -> VerificationResult {
|
||
guard !isVerificationRunning else { return .verificationInProgress }
|
||
isVerificationRunning = true
|
||
defer { isVerificationRunning = false }
|
||
|
||
testLog = ""
|
||
let log = { (msg: String) in self.testLog += msg + "\n" }
|
||
|
||
log("======== 代理验证测试 ========")
|
||
log("App 版本: \(appVersion)")
|
||
log("系统版本: iOS \(UIDevice.current.systemVersion)")
|
||
log("测试目标: lat=\(testLat), lon=\(testLon)")
|
||
log("")
|
||
|
||
// Step A: Proxy running
|
||
log("[步骤 A] 检查代理是否运行…")
|
||
log(" 端口: 127.0.0.1:8888")
|
||
let stepAStart = Date()
|
||
if !proxy.isRunning {
|
||
log(" ⚠ 代理未运行,尝试启动…")
|
||
do { try await proxy.start() } catch {
|
||
log(" ✗ 启动失败: \(error.localizedDescription)")
|
||
return .proxyNotRunning
|
||
}
|
||
log(" ✓ 代理启动成功")
|
||
} else {
|
||
log(" ✓ 代理已在运行中")
|
||
}
|
||
collectProxyLogs(since: stepAStart, to: log)
|
||
|
||
// Verification must never overwrite a newer location action.
|
||
let previousCoordinates = proxy.coordinateSnapshot(
|
||
accuracy: WlocSettingsStore.load()?.accuracy ?? 25
|
||
)
|
||
var verificationCoordinateRevision: UInt64?
|
||
defer {
|
||
if let revision = verificationCoordinateRevision {
|
||
let restored = proxy.restoreCoords(previousCoordinates, ifUnchangedSince: revision)
|
||
log(restored ? " ↩ 已恢复验证前的代理坐标" : " ↩ 检测到更新位置,跳过旧坐标恢复")
|
||
}
|
||
}
|
||
|
||
// Step B: Combined CA + WiFi proxy check (single request)
|
||
log("")
|
||
log("[步骤 B] 检测证书与 WiFi 代理…")
|
||
log(" 方式: 请求 baidu.com/paopao-verify-<token>")
|
||
log(" 结果判定: TLS 错误=证书问题 / 响应不匹配=代理未配置 / 匹配=通过")
|
||
let stepBStart = Date()
|
||
let verifyToken = CoreBridge.refreshVerifyToken()
|
||
guard !verifyToken.isEmpty else {
|
||
log(" ✗ 无法生成验证 token")
|
||
return .certNotTrusted
|
||
}
|
||
do {
|
||
let url = URL(string: "https://www.baidu.com/paopao-verify-\(verifyToken)")!
|
||
var req = URLRequest(url: url)
|
||
req.timeoutInterval = 8
|
||
req.cachePolicy = .reloadIgnoringLocalAndRemoteCacheData
|
||
let config = URLSessionConfiguration.ephemeral
|
||
let (data, resp) = try await URLSession(configuration: config).data(for: req)
|
||
let statusCode = (resp as? HTTPURLResponse)?.statusCode ?? 0
|
||
let body = String(data: data, encoding: .utf8) ?? ""
|
||
if body == verifyToken {
|
||
log(" ✓ 证书已信任,WiFi 代理已配置")
|
||
} else {
|
||
log(" ✗ 响应不匹配 (HTTP \(statusCode)),WiFi 代理未配置")
|
||
log(" 收到: \(body.prefix(100))")
|
||
return .wifiProxyNotConfigured
|
||
}
|
||
} catch {
|
||
let ns = error as NSError
|
||
let msg = error.localizedDescription
|
||
log(" ✗ 请求失败 [\(ns.domain) code=\(ns.code)]: \(msg)")
|
||
if ns.domain == NSURLErrorDomain && ns.code == -1202 {
|
||
log(" TLS 握手被拒,CA 证书未信任")
|
||
return .certNotTrusted
|
||
}
|
||
if msg.contains("TLS") {
|
||
log(" 包含 TLS → 证书问题")
|
||
return .certNotTrusted
|
||
}
|
||
return .wifiProxyNotConfigured
|
||
}
|
||
collectProxyLogs(since: stepBStart, to: log)
|
||
|
||
// Step C: Write and verify coordinates
|
||
log("[步骤 C] 写入测试坐标并验证…")
|
||
guard let testCoordinateRevision = proxy.setCoordsIfUnchanged(
|
||
lat: testLat,
|
||
lon: testLon,
|
||
enabled: true,
|
||
accuracy: 25,
|
||
expectedRevision: previousCoordinates.revision
|
||
) else {
|
||
log(" ↪ 检测到更新位置,取消过期验证坐标写入")
|
||
return .verificationSuperseded
|
||
}
|
||
verificationCoordinateRevision = testCoordinateRevision
|
||
let coords = proxy.getCoords()
|
||
if coords.enabled && abs(coords.lat - testLat) < 0.001 && abs(coords.lon - testLon) < 0.001 {
|
||
log(" ✓ 坐标写入成功: lat=\(coords.lat) lon=\(coords.lon)")
|
||
} else {
|
||
log(" ✗ 坐标验证失败: enabled=\(coords.enabled) lat=\(coords.lat) lon=\(coords.lon)")
|
||
return .coordinateWriteFailed("写入后回读不一致")
|
||
}
|
||
|
||
// Step D: verify data rewriting via Go test patch
|
||
log("[步骤 D] 验证定位数据改写…")
|
||
let result = CoreBridge.testWlocPatch(lat: testLat, lon: testLon, accuracy: 25)
|
||
log(" \(result)")
|
||
if result.hasPrefix("ok:") {
|
||
log(" ✓ 定位数据改写验证通过")
|
||
} else {
|
||
log(" ✗ 定位数据改写失败")
|
||
return .patchFailed(result)
|
||
}
|
||
|
||
log("")
|
||
log("======== 环境检测通过 ✓ ========")
|
||
return .success
|
||
}
|
||
|
||
/// 拉取 Go 代理的详细日志(CONNECT/请求/上游响应/改写结果)到 testLog
|
||
private func collectProxyLogs(since date: Date, to log: (String) -> Void) {
|
||
CoreBridge.flushLogs(category: "Proxy")
|
||
let entries = RuntimeLogStore.loadAll(limit: 200)
|
||
let proxyEntries = entries.filter {
|
||
$0.source == "CORE" && $0.category == "Proxy" && $0.timestamp >= date
|
||
}
|
||
guard !proxyEntries.isEmpty else { return }
|
||
log(" --- 代理日志 ---")
|
||
for e in proxyEntries {
|
||
log(" " + e.message)
|
||
}
|
||
}
|
||
}
|