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:
@@ -6,7 +6,11 @@ struct BreakpointRulesView: View {
|
||||
@State private var isEnabled = IPCManager.shared.isBreakpointEnabled
|
||||
@State private var rules: [BreakpointRule] = []
|
||||
@State private var showAddRule = false
|
||||
@State private var editingRule: BreakpointRule?
|
||||
@State private var ruleToDelete: BreakpointRule?
|
||||
@State private var showDeleteConfirmation = false
|
||||
@State private var observation: AnyDatabaseCancellable?
|
||||
@State private var selectedTab = 0
|
||||
|
||||
private let rulesRepo = RulesRepository()
|
||||
|
||||
@@ -25,42 +29,66 @@ struct BreakpointRulesView: View {
|
||||
}
|
||||
.listRowInsets(EdgeInsets())
|
||||
|
||||
Section("Rules") {
|
||||
if rules.isEmpty {
|
||||
Section {
|
||||
Picker("Tab", selection: $selectedTab) {
|
||||
Text("Rules").tag(0)
|
||||
Text("Waiting").tag(1)
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
.listRowInsets(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16))
|
||||
.listRowBackground(Color.clear)
|
||||
}
|
||||
|
||||
if selectedTab == 0 {
|
||||
Section("Rules") {
|
||||
if rules.isEmpty {
|
||||
EmptyStateView(
|
||||
icon: "pause.circle",
|
||||
title: "No Breakpoint Rules",
|
||||
subtitle: "Tap + to create a new breakpoint rule."
|
||||
)
|
||||
} else {
|
||||
ForEach(rules) { rule in
|
||||
Button {
|
||||
editingRule = rule
|
||||
} label: {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(rule.name ?? rule.urlPattern)
|
||||
.font(.subheadline.weight(.medium))
|
||||
Text(rule.urlPattern)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
HStack(spacing: 8) {
|
||||
if rule.interceptRequest {
|
||||
Text("Request")
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.blue)
|
||||
}
|
||||
if rule.interceptResponse {
|
||||
Text("Response")
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.green)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.tint(.primary)
|
||||
}
|
||||
.onDelete { indexSet in
|
||||
if let index = indexSet.first {
|
||||
ruleToDelete = rules[index]
|
||||
showDeleteConfirmation = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Section("Waiting") {
|
||||
EmptyStateView(
|
||||
icon: "pause.circle",
|
||||
title: "No Breakpoint Rules",
|
||||
subtitle: "Tap + to create a new breakpoint rule."
|
||||
icon: "clock",
|
||||
title: "No Waiting Breakpoints",
|
||||
subtitle: "Breakpoints will appear here when a request is paused."
|
||||
)
|
||||
} else {
|
||||
ForEach(rules) { rule in
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(rule.name ?? rule.urlPattern)
|
||||
.font(.subheadline.weight(.medium))
|
||||
Text(rule.urlPattern)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
HStack(spacing: 8) {
|
||||
if rule.interceptRequest {
|
||||
Text("Request")
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.blue)
|
||||
}
|
||||
if rule.interceptResponse {
|
||||
Text("Response")
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.green)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.onDelete { indexSet in
|
||||
for index in indexSet {
|
||||
if let id = rules[index].id {
|
||||
try? rulesRepo.deleteBreakpointRule(id: id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -73,17 +101,15 @@ struct BreakpointRulesView: View {
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showAddRule) {
|
||||
NavigationStack {
|
||||
Form {
|
||||
// TODO: Add breakpoint rule creation form
|
||||
Text("Breakpoint rule creation")
|
||||
}
|
||||
.navigationTitle("New Breakpoint Rule")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button("Cancel") { showAddRule = false }
|
||||
}
|
||||
AddBreakpointRuleSheet(rulesRepo: rulesRepo, isPresented: $showAddRule)
|
||||
}
|
||||
.sheet(item: $editingRule) { rule in
|
||||
AddBreakpointRuleSheet(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.deleteBreakpointRule(id: id)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -97,3 +123,101 @@ struct BreakpointRulesView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Add Breakpoint Rule Sheet
|
||||
|
||||
private struct AddBreakpointRuleSheet: View {
|
||||
let rulesRepo: RulesRepository
|
||||
let existingRule: BreakpointRule?
|
||||
@Binding var isPresented: Bool
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
@State private var name = ""
|
||||
@State private var urlPattern = ""
|
||||
@State private var method = "ANY"
|
||||
@State private var interceptRequest = true
|
||||
@State private var interceptResponse = true
|
||||
|
||||
private var methods: [String] { ["ANY"] + ProxyConstants.httpMethods }
|
||||
|
||||
init(rulesRepo: RulesRepository, existingRule: BreakpointRule? = nil, isPresented: Binding<Bool>) {
|
||||
self.rulesRepo = rulesRepo
|
||||
self.existingRule = existingRule
|
||||
self._isPresented = isPresented
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
Form {
|
||||
Section {
|
||||
TextField("Name (optional)", text: $name)
|
||||
TextField("URL Pattern (e.g. */api/*)", text: $urlPattern)
|
||||
.textInputAutocapitalization(.never)
|
||||
.autocorrectionDisabled()
|
||||
}
|
||||
|
||||
Section {
|
||||
Picker("Method", selection: $method) {
|
||||
ForEach(methods, id: \.self) { m in
|
||||
Text(m).tag(m)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Section {
|
||||
Toggle("Intercept Request", isOn: $interceptRequest)
|
||||
Toggle("Intercept Response", isOn: $interceptResponse)
|
||||
}
|
||||
|
||||
Section {
|
||||
Button("Save") {
|
||||
if let existing = existingRule {
|
||||
let updated = BreakpointRule(
|
||||
id: existing.id,
|
||||
name: name.isEmpty ? nil : name,
|
||||
urlPattern: urlPattern,
|
||||
method: method,
|
||||
interceptRequest: interceptRequest,
|
||||
interceptResponse: interceptResponse,
|
||||
isEnabled: existing.isEnabled,
|
||||
createdAt: existing.createdAt
|
||||
)
|
||||
try? rulesRepo.updateBreakpointRule(updated)
|
||||
} else {
|
||||
var rule = BreakpointRule(
|
||||
name: name.isEmpty ? nil : name,
|
||||
urlPattern: urlPattern,
|
||||
method: method,
|
||||
interceptRequest: interceptRequest,
|
||||
interceptResponse: interceptResponse
|
||||
)
|
||||
try? rulesRepo.insertBreakpointRule(&rule)
|
||||
}
|
||||
isPresented = false
|
||||
dismiss()
|
||||
}
|
||||
.disabled(urlPattern.isEmpty)
|
||||
}
|
||||
}
|
||||
.navigationTitle(existingRule == nil ? "New Breakpoint Rule" : "Edit Breakpoint Rule")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button("Cancel") {
|
||||
isPresented = false
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
if let rule = existingRule {
|
||||
name = rule.name ?? ""
|
||||
urlPattern = rule.urlPattern
|
||||
method = rule.method
|
||||
interceptRequest = rule.interceptRequest
|
||||
interceptResponse = rule.interceptResponse
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user