- 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
171 lines
6.5 KiB
Swift
171 lines
6.5 KiB
Swift
import SwiftUI
|
|
import ProxyCore
|
|
import GRDB
|
|
|
|
struct DNSSpoofingView: View {
|
|
@State private var isEnabled = IPCManager.shared.isDNSSpoofingEnabled
|
|
@State private var rules: [DNSSpoofRule] = []
|
|
@State private var showAddRule = false
|
|
@State private var editingRule: DNSSpoofRule?
|
|
@State private var ruleToDelete: DNSSpoofRule?
|
|
@State private var showDeleteConfirmation = false
|
|
@State private var observation: AnyDatabaseCancellable?
|
|
|
|
private let rulesRepo = RulesRepository()
|
|
|
|
var body: some View {
|
|
List {
|
|
Section {
|
|
ToggleHeaderView(
|
|
title: "DNS Spoofing",
|
|
description: "Redirect domain resolution to a different target. Useful for routing production domains to development servers.",
|
|
isEnabled: $isEnabled
|
|
)
|
|
.onChange(of: isEnabled) { _, newValue in
|
|
IPCManager.shared.isDNSSpoofingEnabled = newValue
|
|
IPCManager.shared.post(.configurationChanged)
|
|
}
|
|
}
|
|
.listRowInsets(EdgeInsets())
|
|
|
|
Section("Rules") {
|
|
if rules.isEmpty {
|
|
EmptyStateView(
|
|
icon: "network",
|
|
title: "No DNS Spoofing Rules",
|
|
subtitle: "Tap + to create a new rule."
|
|
)
|
|
} else {
|
|
ForEach(rules) { rule in
|
|
Button {
|
|
editingRule = rule
|
|
} label: {
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
HStack {
|
|
Text(rule.sourceDomain)
|
|
.font(.subheadline)
|
|
Image(systemName: "arrow.right")
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
Text(rule.targetDomain)
|
|
.font(.subheadline)
|
|
.foregroundStyle(.blue)
|
|
}
|
|
}
|
|
}
|
|
.tint(.primary)
|
|
}
|
|
.onDelete { indexSet in
|
|
if let index = indexSet.first {
|
|
ruleToDelete = rules[index]
|
|
showDeleteConfirmation = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.navigationTitle("DNS Spoofing")
|
|
.toolbar {
|
|
ToolbarItem(placement: .topBarTrailing) {
|
|
Button { showAddRule = true } label: {
|
|
Image(systemName: "plus")
|
|
}
|
|
}
|
|
}
|
|
.sheet(isPresented: $showAddRule) {
|
|
AddDNSSpoofRuleSheet(rulesRepo: rulesRepo, isPresented: $showAddRule)
|
|
}
|
|
.sheet(item: $editingRule) { rule in
|
|
AddDNSSpoofRuleSheet(rulesRepo: rulesRepo, existingRule: rule, isPresented: .constant(true))
|
|
}
|
|
.confirmationDialog("Delete this rule?", isPresented: $showDeleteConfirmation, presenting: ruleToDelete) { rule in
|
|
Button("Delete", role: .destructive) {
|
|
if let id = rule.id {
|
|
try? rulesRepo.deleteDNSSpoofRule(id: id)
|
|
}
|
|
}
|
|
}
|
|
.task {
|
|
observation = rulesRepo.observeDNSSpoofRules()
|
|
.start(in: DatabaseManager.shared.dbPool) { error in
|
|
print("DNS Spoof observation error: \(error)")
|
|
} onChange: { newRules in
|
|
rules = newRules
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Add DNS Spoof Rule Sheet
|
|
|
|
private struct AddDNSSpoofRuleSheet: View {
|
|
let rulesRepo: RulesRepository
|
|
let existingRule: DNSSpoofRule?
|
|
@Binding var isPresented: Bool
|
|
@Environment(\.dismiss) private var dismiss
|
|
|
|
@State private var sourceDomain = ""
|
|
@State private var targetDomain = ""
|
|
|
|
init(rulesRepo: RulesRepository, existingRule: DNSSpoofRule? = nil, isPresented: Binding<Bool>) {
|
|
self.rulesRepo = rulesRepo
|
|
self.existingRule = existingRule
|
|
self._isPresented = isPresented
|
|
}
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
Form {
|
|
Section {
|
|
TextField("Source Domain (e.g. api.example.com)", text: $sourceDomain)
|
|
.textInputAutocapitalization(.never)
|
|
.autocorrectionDisabled()
|
|
TextField("Target Domain (e.g. dev.example.com)", text: $targetDomain)
|
|
.textInputAutocapitalization(.never)
|
|
.autocorrectionDisabled()
|
|
}
|
|
|
|
Section {
|
|
Button("Save") {
|
|
if let existing = existingRule {
|
|
let updated = DNSSpoofRule(
|
|
id: existing.id,
|
|
sourceDomain: sourceDomain,
|
|
targetDomain: targetDomain,
|
|
isEnabled: existing.isEnabled,
|
|
createdAt: existing.createdAt
|
|
)
|
|
try? rulesRepo.updateDNSSpoofRule(updated)
|
|
} else {
|
|
var rule = DNSSpoofRule(
|
|
sourceDomain: sourceDomain,
|
|
targetDomain: targetDomain
|
|
)
|
|
try? rulesRepo.insertDNSSpoofRule(&rule)
|
|
}
|
|
isPresented = false
|
|
dismiss()
|
|
}
|
|
.disabled(sourceDomain.isEmpty || targetDomain.isEmpty)
|
|
}
|
|
}
|
|
.navigationTitle(existingRule == nil ? "New DNS Rule" : "Edit DNS Rule")
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.toolbar {
|
|
ToolbarItem(placement: .cancellationAction) {
|
|
Button("Cancel") {
|
|
isPresented = false
|
|
dismiss()
|
|
}
|
|
}
|
|
}
|
|
.onAppear {
|
|
if let rule = existingRule {
|
|
sourceDomain = rule.sourceDomain
|
|
targetDomain = rule.targetDomain
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|