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 构建
302 lines
15 KiB
Swift
302 lines
15 KiB
Swift
import SwiftUI
|
|
|
|
enum SetupStep: Int, CaseIterable {
|
|
case cert = 0, proxy = 1, verify = 2
|
|
var title: String {
|
|
switch self {
|
|
case .cert: return "初始化 CA 证书"
|
|
case .proxy: return "初始化代理"
|
|
case .verify: return "环境检测"
|
|
}
|
|
}
|
|
}
|
|
|
|
struct FirstSetupView: View {
|
|
@ObservedObject var setup: SetupCoordinator
|
|
let onComplete: () -> Void
|
|
|
|
@State private var step: SetupStep = .cert
|
|
@State private var downloadedDone = false
|
|
@State private var installedDone = false
|
|
@State private var trustedDone = false
|
|
@State private var proxyDone = false
|
|
@State private var testPassed: VerificationResult? = nil
|
|
@State private var testMessage = ""
|
|
@State private var isLoading = false
|
|
@State private var manualHint = ""
|
|
|
|
private var certDone: Bool { downloadedDone && installedDone && trustedDone }
|
|
|
|
var body: some View {
|
|
VStack(spacing: 0) {
|
|
// 顶部大步骤进度
|
|
HStack(spacing: 6) {
|
|
ForEach(SetupStep.allCases, id: \.rawValue) { s in
|
|
VStack(spacing: 4) {
|
|
Circle()
|
|
.fill(s.rawValue < step.rawValue ? Color.green
|
|
: s.rawValue == step.rawValue ? Color.blue
|
|
: Color.gray.opacity(0.3))
|
|
.frame(width: 10, height: 10)
|
|
Text(s.title).font(.caption2).foregroundStyle(.secondary)
|
|
}
|
|
if s.rawValue < SetupStep.allCases.count - 1 {
|
|
Rectangle().fill(s.rawValue < step.rawValue ? Color.green : Color.gray.opacity(0.3))
|
|
.frame(height: 2).frame(maxWidth: 30)
|
|
}
|
|
}
|
|
}
|
|
.padding(.top, 18)
|
|
|
|
ScrollView {
|
|
VStack(alignment: .leading, spacing: 20) {
|
|
switch step {
|
|
case .cert: certStepView
|
|
case .proxy: proxyStepView
|
|
case .verify: verifyStepView
|
|
}
|
|
}
|
|
.padding(20)
|
|
}
|
|
|
|
// 底部导航
|
|
Divider()
|
|
HStack {
|
|
if step.rawValue > 0 {
|
|
Button("上一步") { step = SetupStep(rawValue: step.rawValue - 1)! }
|
|
.buttonStyle(.bordered)
|
|
}
|
|
Spacer()
|
|
if step == .verify && testPassed?.isSuccess == true {
|
|
Button("完成,进入主页") { onComplete() }.buttonStyle(.borderedProminent)
|
|
} else if step == .cert && certDone {
|
|
Button("下一步") { step = .proxy }.buttonStyle(.borderedProminent)
|
|
} else if step == .proxy && proxyDone {
|
|
Button("下一步") { step = .verify }.buttonStyle(.borderedProminent)
|
|
}
|
|
}
|
|
.padding(.horizontal, 20).padding(.vertical, 10)
|
|
}
|
|
.alert("无法直接跳转", isPresented: Binding(
|
|
get: { !manualHint.isEmpty },
|
|
set: { if !$0 { manualHint = "" } }
|
|
)) {
|
|
Button("知道了", role: .cancel) {}
|
|
} message: { Text(manualHint) }
|
|
}
|
|
|
|
// MARK: - 第 1 步:初始化 CA 证书(三个步骤平铺)
|
|
|
|
private var certStepView: some View {
|
|
VStack(alignment: .leading, spacing: 20) {
|
|
GroupBox(label: Label("第 1 步:下载证书", systemImage: "arrow.down.circle")) {
|
|
VStack(alignment: .leading, spacing: 12) {
|
|
Color.clear.frame(height: 0).padding(.top, 2)
|
|
Text("虚拟定位需要通过自签 CA 证书来解密和改写定位请求。点击下方按钮,Safari 会打开下载页面。Safari 弹出「此网站正尝试下载一个配置描述文件」时,点「允许」。")
|
|
.font(.caption).foregroundStyle(.secondary)
|
|
HStack(spacing: 10) {
|
|
Button {
|
|
Task { await setup.proxy.openCertificateDownload() }
|
|
} label: {
|
|
Label("去下载", systemImage: "arrow.down.circle.fill").frame(maxWidth: .infinity)
|
|
}.buttonStyle(.borderedProminent).tint(.blue)
|
|
Button {
|
|
downloadedDone = true
|
|
} label: {
|
|
Label(downloadedDone ? "已完成 ✓" : "已完成", systemImage: downloadedDone ? "checkmark.circle.fill" : "circle").frame(maxWidth: .infinity)
|
|
}.buttonStyle(.bordered).tint(downloadedDone ? .green : .secondary)
|
|
}
|
|
}
|
|
}
|
|
|
|
GroupBox(label: Label("第 2 步:安装证书", systemImage: "square.and.arrow.down")) {
|
|
VStack(alignment: .leading, spacing: 12) {
|
|
Color.clear.frame(height: 0).padding(.top, 2)
|
|
Text("下载完成后打开系统「设置」:\n\n1. 如果顶部显示了「已下载描述文件」,点进去安装\n2. 如果没显示:进入「通用 → VPN与设备管理」,找到 WLOC CA 证书点击安装\n\n安装时系统会要求输入锁屏密码,确认后点右上角「安装」即可。")
|
|
.font(.caption).foregroundStyle(.secondary)
|
|
HStack(spacing: 10) {
|
|
Button { openSettings(.general) } label: {
|
|
Label("去安装", systemImage: "gearshape").frame(maxWidth: .infinity)
|
|
}.buttonStyle(.borderedProminent).tint(.blue)
|
|
Button { installedDone = true } label: {
|
|
Label(installedDone ? "已完成 ✓" : "已完成", systemImage: installedDone ? "checkmark.circle.fill" : "circle").frame(maxWidth: .infinity)
|
|
}.buttonStyle(.bordered).tint(installedDone ? .green : .secondary)
|
|
}
|
|
}
|
|
}
|
|
|
|
GroupBox(label: Label("第 3 步:信任证书", systemImage: "shield.checkered")) {
|
|
VStack(alignment: .leading, spacing: 12) {
|
|
Color.clear.frame(height: 0).padding(.top, 2)
|
|
Text("证书安装后还需要开启信任,否则系统会拦截代理的 HTTPS 请求:\n\n1. 打开「设置 → 通用 → 关于本机」\n2. 滑到底部找到「证书信任设置」\n3. 找到 WLOC CA,打开旁边的开关\n4. 弹出的警告中点「继续」\n\n⚠️ 每次重装 App 都需要重新下载安装证书。如果检测时报 TLS 错误,说明证书过期或不匹配,请删除旧证书(设置 → 通用 → VPN与设备管理)后重新安装。")
|
|
.font(.caption).foregroundStyle(.secondary)
|
|
HStack(spacing: 10) {
|
|
Button { openSettings(.general) } label: {
|
|
Label("去信任", systemImage: "shield.checkered").frame(maxWidth: .infinity)
|
|
}.buttonStyle(.borderedProminent).tint(.blue)
|
|
Button { trustedDone = true } label: {
|
|
Label(trustedDone ? "已完成 ✓" : "已完成", systemImage: trustedDone ? "checkmark.circle.fill" : "circle").frame(maxWidth: .infinity)
|
|
}.buttonStyle(.bordered).tint(trustedDone ? .green : .secondary)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - 第 2 步:初始化代理
|
|
|
|
private var proxyStepView: some View {
|
|
VStack(alignment: .leading, spacing: 20) {
|
|
GroupBox(label: Label("配置 WiFi 系统代理", systemImage: "wifi")) {
|
|
VStack(alignment: .leading, spacing: 12) {
|
|
Color.clear.frame(height: 0).padding(.top, 2)
|
|
Text("要让系统定位请求经过本地代理,需要在 WiFi 设置中手动配置:\n\n1. 打开「设置 → 无线局域网」\n2. 点击当前连接的 WiFi 右侧 (i) 图标\n3. 滑到底部找到「HTTP 代理」,选择「手动」\n4. 服务器填入 127.0.0.1,端口填入 8888\n5. 点右上角「存储」\n\n⚠️ 只有连接的这个 WiFi 会走代理,蜂窝数据不受影响。换 WiFi 后需要重新配置。")
|
|
.font(.caption).foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
|
|
HStack(spacing: 12) {
|
|
Button {
|
|
UIPasteboard.general.string = "127.0.0.1:8888"
|
|
} label: {
|
|
Label("复制地址", systemImage: "doc.on.doc")
|
|
.frame(maxWidth: .infinity)
|
|
}.buttonStyle(.bordered).tint(.blue)
|
|
Button {
|
|
openSettings(.wifi)
|
|
} label: {
|
|
Label("去设置", systemImage: "wifi")
|
|
.frame(maxWidth: .infinity)
|
|
}.buttonStyle(.borderedProminent).tint(.blue)
|
|
}
|
|
|
|
Button {
|
|
proxyDone = true
|
|
} label: {
|
|
Label(proxyDone ? "已完成 ✓" : "已完成", systemImage: proxyDone ? "checkmark.circle.fill" : "circle")
|
|
.frame(maxWidth: .infinity)
|
|
}.buttonStyle(.bordered).tint(proxyDone ? .green : .secondary)
|
|
}
|
|
}
|
|
|
|
// MARK: - 第 3 步:环境检测
|
|
|
|
private var verifyStepView: some View {
|
|
VStack(alignment: .leading, spacing: 20) {
|
|
GroupBox(label: Label("检测整个流程", systemImage: "checklist")) {
|
|
VStack(alignment: .leading, spacing: 12) {
|
|
Color.clear.frame(height: 0).padding(.top, 2)
|
|
Text("点击「开始检测」后依次检查代理运行、证书信任与 WiFi 代理配置、坐标写入、数据改写。全部通过后「完成,进入主页」按钮才会亮起。")
|
|
.font(.caption).foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
|
|
if let result = testPassed, result.isSuccess {
|
|
HStack(spacing: 10) {
|
|
Image(systemName: "checkmark.circle.fill").foregroundStyle(.green).font(.title3)
|
|
Text("全部检测通过").font(.subheadline.weight(.semibold)).foregroundStyle(.green)
|
|
}.padding(12).background(Color.green.opacity(0.08), in: RoundedRectangle(cornerRadius: 10))
|
|
} else if let result = testPassed {
|
|
VStack(alignment: .leading, spacing: 10) {
|
|
HStack(spacing: 10) {
|
|
Image(systemName: "xmark.circle.fill").foregroundStyle(.red).font(.title3)
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
Text("测试未通过").font(.subheadline.weight(.semibold)).foregroundStyle(.red)
|
|
Text(failureSummary(result)).font(.caption).foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
if let step = failureRemedyStep(result) {
|
|
Button {
|
|
withAnimation(.none) { self.step = step }
|
|
testPassed = nil
|
|
testMessage = ""
|
|
} label: {
|
|
Label("去处理", systemImage: "arrow.right.circle.fill")
|
|
.font(.subheadline.weight(.medium))
|
|
.frame(maxWidth: .infinity)
|
|
.padding(.vertical, 10)
|
|
}
|
|
.buttonStyle(.borderedProminent)
|
|
.tint(.red)
|
|
}
|
|
}
|
|
.padding(12)
|
|
.background(Color.red.opacity(0.08), in: RoundedRectangle(cornerRadius: 10))
|
|
}
|
|
|
|
if !testMessage.isEmpty {
|
|
ScrollView {
|
|
Text(testMessage)
|
|
.font(.caption.monospaced()).textSelection(.enabled)
|
|
.padding(10).frame(maxWidth: .infinity, alignment: .leading)
|
|
.background(Color(.secondarySystemBackground), in: RoundedRectangle(cornerRadius: 8))
|
|
}.frame(maxHeight: 280)
|
|
}
|
|
|
|
Button {
|
|
isLoading = true; testMessage = ""
|
|
Task {
|
|
testPassed = await setup.runVerificationTest()
|
|
testMessage = setup.testLog
|
|
isLoading = false
|
|
}
|
|
} label: {
|
|
HStack {
|
|
if isLoading { ProgressView().tint(.white).controlSize(.small) }
|
|
Text(buttonText).frame(maxWidth: .infinity)
|
|
}
|
|
}
|
|
.buttonStyle(.borderedProminent)
|
|
.tint(buttonTint)
|
|
.disabled(isLoading)
|
|
|
|
Button {
|
|
UIPasteboard.general.string = testMessage
|
|
} label: {
|
|
Label("复制检测日志", systemImage: "doc.on.doc").frame(maxWidth: .infinity)
|
|
}.buttonStyle(.bordered).disabled(testMessage.isEmpty)
|
|
}
|
|
}
|
|
|
|
private var buttonText: String {
|
|
guard let result = testPassed else { return "开始检测" }
|
|
return result.isSuccess ? "重新检测" : "⚠️ 重新测试"
|
|
}
|
|
|
|
private var buttonTint: Color {
|
|
guard let result = testPassed else { return .blue }
|
|
return result.isSuccess ? .blue : .red
|
|
}
|
|
|
|
private func failureSummary(_ result: VerificationResult) -> String {
|
|
switch result {
|
|
case .proxyNotRunning: return "代理未能启动,请检查代理状态"
|
|
case .verificationInProgress: return "已有检测正在进行,请稍候"
|
|
case .verificationSuperseded: return "检测期间位置已更新,本次结果已取消,请重新检测"
|
|
case .certNotTrusted: return "CA 证书未安装或未信任,请重新安装证书并开启信任"
|
|
case .wifiProxyNotConfigured: return "WiFi 代理未配置,请在系统设置中配置 127.0.0.1:8888"
|
|
case .coordinateWriteFailed: return "坐标写入失败,请重试"
|
|
case .patchFailed: return "坐标改写验证失败,可能是证书过期或不匹配"
|
|
case .success: return ""
|
|
}
|
|
}
|
|
|
|
private func failureRemedyStep(_ result: VerificationResult) -> SetupStep? {
|
|
switch result {
|
|
case .certNotTrusted: return .cert
|
|
case .wifiProxyNotConfigured: return .proxy
|
|
case .proxyNotRunning, .verificationInProgress, .verificationSuperseded,
|
|
.coordinateWriteFailed, .patchFailed: return nil
|
|
case .success: return nil
|
|
}
|
|
}
|
|
|
|
|
|
@MainActor
|
|
private func openSettings(_ destination: SystemSettingsDestination) {
|
|
SystemSettingsNavigator.open(destination) { fallbackHint in
|
|
if let fallbackHint { manualHint = fallbackHint }
|
|
}
|
|
}
|
|
}
|