- 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
201 lines
7.3 KiB
Swift
201 lines
7.3 KiB
Swift
import SwiftUI
|
|
import ProxyCore
|
|
import GRDB
|
|
|
|
struct SSLProxyingListView: View {
|
|
@State private var isEnabled = IPCManager.shared.isSSLProxyingEnabled
|
|
@State private var entries: [SSLProxyingEntry] = []
|
|
@State private var showAddInclude = false
|
|
@State private var showAddExclude = false
|
|
@State private var editingEntry: SSLProxyingEntry?
|
|
@State private var entryToDelete: SSLProxyingEntry?
|
|
@State private var showDeleteConfirmation = false
|
|
@State private var observation: AnyDatabaseCancellable?
|
|
|
|
private let rulesRepo = RulesRepository()
|
|
|
|
var includeEntries: [SSLProxyingEntry] {
|
|
entries.filter(\.isInclude)
|
|
}
|
|
|
|
var excludeEntries: [SSLProxyingEntry] {
|
|
entries.filter { !$0.isInclude }
|
|
}
|
|
|
|
var body: some View {
|
|
List {
|
|
Section {
|
|
ToggleHeaderView(
|
|
title: "SSL Proxying",
|
|
description: "Decrypt HTTPS traffic from included domains. Excluded domains are always passed through.",
|
|
isEnabled: $isEnabled
|
|
)
|
|
.onChange(of: isEnabled) { _, newValue in
|
|
IPCManager.shared.isSSLProxyingEnabled = newValue
|
|
IPCManager.shared.post(.configurationChanged)
|
|
}
|
|
}
|
|
.listRowInsets(EdgeInsets())
|
|
|
|
Section("Include") {
|
|
if includeEntries.isEmpty {
|
|
Text("No include entries")
|
|
.foregroundStyle(.secondary)
|
|
.font(.subheadline)
|
|
} else {
|
|
ForEach(includeEntries) { entry in
|
|
Button {
|
|
editingEntry = entry
|
|
} label: {
|
|
Text(entry.domainPattern)
|
|
.foregroundStyle(.primary)
|
|
}
|
|
}
|
|
.onDelete { indexSet in
|
|
if let index = indexSet.first {
|
|
entryToDelete = includeEntries[index]
|
|
showDeleteConfirmation = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Section("Exclude") {
|
|
if excludeEntries.isEmpty {
|
|
Text("No exclude entries")
|
|
.foregroundStyle(.secondary)
|
|
.font(.subheadline)
|
|
} else {
|
|
ForEach(excludeEntries) { entry in
|
|
Button {
|
|
editingEntry = entry
|
|
} label: {
|
|
Text(entry.domainPattern)
|
|
.foregroundStyle(.primary)
|
|
}
|
|
}
|
|
.onDelete { indexSet in
|
|
if let index = indexSet.first {
|
|
entryToDelete = excludeEntries[index]
|
|
showDeleteConfirmation = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.navigationTitle("SSL Proxying")
|
|
.toolbar {
|
|
ToolbarItem(placement: .topBarTrailing) {
|
|
Menu {
|
|
Button("Add Include Entry") { showAddInclude = true }
|
|
Button("Add Exclude Entry") { showAddExclude = true }
|
|
Divider()
|
|
Button("Clear All Rules", role: .destructive) {
|
|
try? rulesRepo.deleteAllSSLEntries()
|
|
IPCManager.shared.post(.configurationChanged)
|
|
}
|
|
} label: {
|
|
Image(systemName: "ellipsis.circle")
|
|
}
|
|
}
|
|
}
|
|
.sheet(isPresented: $showAddInclude) {
|
|
DomainEntrySheet(title: "New Include Entry", isInclude: true) { pattern in
|
|
var entry = SSLProxyingEntry(domainPattern: pattern, isInclude: true)
|
|
try? rulesRepo.insertSSLEntry(&entry)
|
|
IPCManager.shared.isSSLProxyingEnabled = true
|
|
IPCManager.shared.post(.configurationChanged)
|
|
}
|
|
}
|
|
.sheet(isPresented: $showAddExclude) {
|
|
DomainEntrySheet(title: "New Exclude Entry", isInclude: false) { pattern in
|
|
var entry = SSLProxyingEntry(domainPattern: pattern, isInclude: false)
|
|
try? rulesRepo.insertSSLEntry(&entry)
|
|
IPCManager.shared.post(.configurationChanged)
|
|
}
|
|
}
|
|
.sheet(item: $editingEntry) { entry in
|
|
DomainEntrySheet(
|
|
title: entry.isInclude ? "Edit Include Entry" : "Edit Exclude Entry",
|
|
isInclude: entry.isInclude,
|
|
existingEntry: entry
|
|
) { pattern in
|
|
var updated = entry
|
|
updated.domainPattern = pattern
|
|
try? rulesRepo.updateSSLEntry(updated)
|
|
if updated.isInclude {
|
|
IPCManager.shared.isSSLProxyingEnabled = true
|
|
}
|
|
IPCManager.shared.post(.configurationChanged)
|
|
}
|
|
}
|
|
.confirmationDialog("Delete this rule?", isPresented: $showDeleteConfirmation, presenting: entryToDelete) { entry in
|
|
Button("Delete", role: .destructive) {
|
|
if let id = entry.id {
|
|
try? rulesRepo.deleteSSLEntry(id: id)
|
|
}
|
|
}
|
|
}
|
|
.task {
|
|
observation = rulesRepo.observeSSLEntries()
|
|
.start(in: DatabaseManager.shared.dbPool) { error in
|
|
print("SSL observation error: \(error)")
|
|
} onChange: { newEntries in
|
|
entries = newEntries
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Domain Entry Sheet
|
|
|
|
struct DomainEntrySheet: View {
|
|
let title: String
|
|
let isInclude: Bool
|
|
let existingEntry: SSLProxyingEntry?
|
|
let onSave: (String) -> Void
|
|
|
|
@State private var domainPattern = ""
|
|
@Environment(\.dismiss) private var dismiss
|
|
|
|
init(title: String, isInclude: Bool, existingEntry: SSLProxyingEntry? = nil, onSave: @escaping (String) -> Void) {
|
|
self.title = title
|
|
self.isInclude = isInclude
|
|
self.existingEntry = existingEntry
|
|
self.onSave = onSave
|
|
}
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
Form {
|
|
Section {
|
|
TextField("Domain Pattern", text: $domainPattern)
|
|
.textInputAutocapitalization(.never)
|
|
.autocorrectionDisabled()
|
|
} footer: {
|
|
Text("Supports wildcards: * (zero or more) and ? (single character). Example: *.example.com")
|
|
}
|
|
}
|
|
.navigationTitle(title)
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.toolbar {
|
|
ToolbarItem(placement: .cancellationAction) {
|
|
Button("Cancel") { dismiss() }
|
|
}
|
|
ToolbarItem(placement: .confirmationAction) {
|
|
Button("Save") {
|
|
onSave(domainPattern)
|
|
dismiss()
|
|
}
|
|
.disabled(domainPattern.isEmpty)
|
|
}
|
|
}
|
|
.onAppear {
|
|
if let entry = existingEntry {
|
|
domainPattern = entry.domainPattern
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|