Files
ProxyIOS/UI/More/SSLProxyingListView.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

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
}
}
}
}
}