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:
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user