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
This commit is contained in:
Trey t
2026-04-11 12:52:18 -05:00
parent c77e506db5
commit 148bc3887c
77 changed files with 6710 additions and 847 deletions

View File

@@ -1,6 +1,8 @@
import SwiftUI
import NetworkExtension
@preconcurrency import NetworkExtension
import LocalAuthentication
import ProxyCore
import GRDB
@Observable
@MainActor
@@ -8,14 +10,44 @@ 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 {
@@ -70,6 +102,19 @@ final class AppState {
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"
@@ -100,4 +145,17 @@ final class AppState {
}
}
}
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
}
}
}
}

View File

@@ -2,46 +2,131 @@ import SwiftUI
struct ContentView: View {
@Environment(AppState.self) private var appState
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
enum Tab: Hashable {
case home, pin, compose, more
enum Tab: String, Hashable, CaseIterable {
case home = "Home"
case pin = "Pin"
case compose = "Compose"
case more = "More"
var icon: String {
switch self {
case .home: return "house.fill"
case .pin: return "pin.fill"
case .compose: return "square.and.pencil"
case .more: return "ellipsis.circle.fill"
}
}
}
@State private var selectedTab: Tab = .home
var body: some View {
TabView(selection: $selectedTab) {
NavigationStack {
HomeView()
Group {
if horizontalSizeClass == .regular {
iPadLayout
} else {
iPhoneLayout
}
.tabItem {
Label("Home", systemImage: "house.fill")
}
.overlay {
if appState.isLocked {
lockScreen
}
.tag(Tab.home)
NavigationStack {
PinView()
}
.tabItem {
Label("Pin", systemImage: "pin.fill")
}
.tag(Tab.pin)
NavigationStack {
ComposeListView()
}
.tabItem {
Label("Compose", systemImage: "square.and.pencil")
}
.tag(Tab.compose)
NavigationStack {
MoreView()
}
.tabItem {
Label("More", systemImage: "ellipsis.circle.fill")
}
.tag(Tab.more)
}
}
private var iPhoneLayout: some View {
TabView(selection: $selectedTab) {
NavigationStack { HomeView() }
.tabItem { Label("Home", systemImage: "house.fill") }
.tag(Tab.home)
NavigationStack { PinView() }
.tabItem { Label("Pin", systemImage: "pin.fill") }
.tag(Tab.pin)
NavigationStack { ComposeListView() }
.tabItem { Label("Compose", systemImage: "square.and.pencil") }
.tag(Tab.compose)
NavigationStack { MoreView() }
.tabItem { Label("More", systemImage: "ellipsis.circle.fill") }
.tag(Tab.more)
}
}
private var iPadLayout: some View {
NavigationSplitView {
iPadSidebar
} detail: {
iPadDetail
}
}
private var iPadSidebar: some View {
VStack(spacing: 4) {
ForEach(Tab.allCases, id: \.self) { tab in
iPadSidebarButton(tab: tab)
}
Spacer()
}
.padding(.top, 8)
.navigationTitle("Proxy")
}
private func iPadSidebarButton(tab: Tab) -> some View {
Button {
selectedTab = tab
} label: {
Label(tab.rawValue, systemImage: tab.icon)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 20)
.padding(.vertical, 14)
.background(
selectedTab == tab ? Color.accentColor.opacity(0.12) : Color.clear,
in: RoundedRectangle(cornerRadius: 10)
)
.foregroundStyle(selectedTab == tab ? Color.accentColor : Color.primary)
}
.buttonStyle(.plain)
.padding(.horizontal, 12)
}
@ViewBuilder
private var iPadDetail: some View {
NavigationStack {
switch selectedTab {
case .home: HomeView()
case .pin: PinView()
case .compose: ComposeListView()
case .more: MoreView()
}
}
}
private var lockScreen: some View {
ZStack {
Color(.systemBackground).ignoresSafeArea()
VStack(spacing: 24) {
Image(systemName: "lock.fill")
.font(.system(size: 48))
.foregroundStyle(.secondary)
Text("Unlock Proxy")
.font(.title2.weight(.semibold))
Button {
appState.authenticate()
} label: {
Text("Unlock")
.font(.headline)
.frame(maxWidth: 300)
.padding()
.background(.blue, in: RoundedRectangle(cornerRadius: 12))
.foregroundStyle(.white)
}
}
}
.onAppear { appState.authenticate() }
}
}

View File

@@ -23,7 +23,7 @@
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
<true/>
</dict>
<key>UILaunchScreen</key>
<dict/>
@@ -31,5 +31,12 @@
<array>
<string>UIInterfaceOrientationPortrait</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>