Files
ProxyIOS/UI/More/SSLProxyingListView.swift
2026-04-06 11:28:40 -05:00

151 lines
5.2 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 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
Text(entry.domainPattern)
}
.onDelete { indexSet in
for index in indexSet {
if let id = includeEntries[index].id {
try? rulesRepo.deleteSSLEntry(id: id)
}
}
}
}
}
Section("Exclude") {
if excludeEntries.isEmpty {
Text("No exclude entries")
.foregroundStyle(.secondary)
.font(.subheadline)
} else {
ForEach(excludeEntries) { entry in
Text(entry.domainPattern)
}
.onDelete { indexSet in
for index in indexSet {
if let id = excludeEntries[index].id {
try? rulesRepo.deleteSSLEntry(id: id)
}
}
}
}
}
}
.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()
}
} 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)
}
}
.sheet(isPresented: $showAddExclude) {
DomainEntrySheet(title: "New Exclude Entry", isInclude: false) { pattern in
var entry = SSLProxyingEntry(domainPattern: pattern, isInclude: false)
try? rulesRepo.insertSSLEntry(&entry)
}
}
.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 onSave: (String) -> Void
@State private var domainPattern = ""
@Environment(\.dismiss) private var dismiss
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)
}
}
}
}
}