Files
ProxyIOS/App/AppState.swift
Trey t 148bc3887c Add iPad support, auto-pinning, and comprehensive logging
- Adaptive iPhone/iPad layout with NavigationSplitView sidebar
- Auto-detect SSL-pinned domains, fall back to passthrough
- Certificate install via local HTTP server (Safari profile flow)
- App Group-backed CA, per-domain leaf cert LRU cache
- DB-backed config repository, Darwin notification throttling
- Rules engine, breakpoint rules, pinned domain tracking
- os.Logger instrumentation across tunnel/proxy/mitm/capture/cert/rules/db/ipc/ui
- Fix dyld framework embed, race conditions, thread safety
2026-04-11 12:52:18 -05:00

162 lines
5.3 KiB
Swift

import SwiftUI
@preconcurrency import NetworkExtension
import LocalAuthentication
import ProxyCore
import GRDB
@Observable
@MainActor
final class AppState {
var vpnStatus: NEVPNStatus = .disconnected
var isCertificateInstalled: Bool = false
var isCertificateTrusted: Bool = false
var isLocked: Bool = false
var runtimeStatus = ProxyRuntimeStatus()
var isAppLockEnabled: Bool {
get { UserDefaults.standard.bool(forKey: "appLockEnabled") }
set { UserDefaults.standard.set(newValue, forKey: "appLockEnabled") }
}
private var vpnManager: NETunnelProviderManager?
private var statusObservation: NSObjectProtocol?
@ObservationIgnored private var runtimeObservation: AnyDatabaseCancellable?
private let runtimeStatusRepo = RuntimeStatusRepository()
init() {
if UserDefaults.standard.bool(forKey: "appLockEnabled") {
isLocked = true
}
isCertificateInstalled = CertificateManager.shared.hasCA
Task {
await loadVPNManager()
}
observeRuntimeStatus()
}
func authenticate() {
let context = LAContext()
var error: NSError?
guard context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &error) else {
isLocked = false
return
}
context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: "Unlock Proxy") { [weak self] success, _ in
Task { @MainActor in
if success {
self?.isLocked = false
}
}
}
}
func loadVPNManager() async {
do {
let managers = try await NETunnelProviderManager.loadAllFromPreferences()
if let existing = managers.first {
vpnManager = existing
} else {
let manager = NETunnelProviderManager()
let proto = NETunnelProviderProtocol()
proto.providerBundleIdentifier = ProxyConstants.extensionBundleIdentifier
proto.serverAddress = ProxyConstants.proxyHost
manager.protocolConfiguration = proto
manager.localizedDescription = "Proxy"
manager.isEnabled = true
try await manager.saveToPreferences()
try await manager.loadFromPreferences()
vpnManager = manager
}
observeVPNStatus()
vpnStatus = vpnManager?.connection.status ?? .disconnected
} catch {
print("[AppState] Failed to load VPN manager: \(error)")
}
}
func toggleVPN() async {
guard let manager = vpnManager else {
await loadVPNManager()
return
}
switch manager.connection.status {
case .connected, .connecting:
manager.connection.stopVPNTunnel()
case .disconnected, .invalid:
do {
// Ensure saved and fresh before starting
manager.isEnabled = true
try await manager.saveToPreferences()
try await manager.loadFromPreferences()
try manager.connection.startVPNTunnel()
} catch {
print("[AppState] Failed to start VPN: \(error)")
}
default:
break
}
}
var isVPNConnected: Bool {
vpnStatus == .connected
}
var hasSharedCertificate: Bool {
guard let localFingerprint = CertificateManager.shared.caFingerprint else { return false }
return localFingerprint == runtimeStatus.caFingerprint
}
var isHTTPSInspectionVerified: Bool {
runtimeStatus.lastSuccessfulMITMAt != nil
}
var lastRuntimeError: String? {
runtimeStatus.lastMITMError ?? runtimeStatus.lastConnectError ?? runtimeStatus.lastProxyError
}
var vpnStatusText: String {
switch vpnStatus {
case .connected: "Connected"
case .connecting: "Connecting..."
case .disconnecting: "Disconnecting..."
case .disconnected: "Disconnected"
case .invalid: "Not Configured"
case .reasserting: "Reconnecting..."
@unknown default: "Unknown"
}
}
private func observeVPNStatus() {
guard let manager = vpnManager else { return }
// Remove existing observer
if let existing = statusObservation {
NotificationCenter.default.removeObserver(existing)
}
statusObservation = NotificationCenter.default.addObserver(
forName: .NEVPNStatusDidChange,
object: manager.connection,
queue: .main
) { [weak self] _ in
Task { @MainActor in
self?.vpnStatus = manager.connection.status
}
}
}
private func observeRuntimeStatus() {
runtimeObservation = runtimeStatusRepo.observeStatus()
.start(in: DatabaseManager.shared.dbPool) { error in
ProxyLogger.ui.error("AppState runtime observation error: \(error.localizedDescription)")
} onChange: { [weak self] status in
Task { @MainActor in
self?.runtimeStatus = status
self?.isCertificateInstalled = CertificateManager.shared.hasCA
self?.isCertificateTrusted = status.lastSuccessfulMITMAt != nil
}
}
}
}