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:
@@ -5,6 +5,9 @@ import GRDB
|
||||
struct MapLocalView: View {
|
||||
@State private var rules: [MapLocalRule] = []
|
||||
@State private var showAddRule = false
|
||||
@State private var editingRule: MapLocalRule?
|
||||
@State private var ruleToDelete: MapLocalRule?
|
||||
@State private var showDeleteConfirmation = false
|
||||
@State private var observation: AnyDatabaseCancellable?
|
||||
|
||||
private let rulesRepo = RulesRepository()
|
||||
@@ -32,22 +35,26 @@ struct MapLocalView: View {
|
||||
)
|
||||
} 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)
|
||||
Text("Status: \(rule.responseStatus)")
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.tertiary)
|
||||
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)
|
||||
Text("Status: \(rule.responseStatus)")
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.tertiary)
|
||||
}
|
||||
}
|
||||
.tint(.primary)
|
||||
}
|
||||
.onDelete { indexSet in
|
||||
for index in indexSet {
|
||||
if let id = rules[index].id {
|
||||
try? rulesRepo.deleteMapLocalRule(id: id)
|
||||
}
|
||||
if let index = indexSet.first {
|
||||
ruleToDelete = rules[index]
|
||||
showDeleteConfirmation = true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -62,17 +69,15 @@ struct MapLocalView: View {
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showAddRule) {
|
||||
NavigationStack {
|
||||
Form {
|
||||
// TODO: Add map local rule creation form
|
||||
Text("Map Local rule creation")
|
||||
}
|
||||
.navigationTitle("New Map Local Rule")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button("Cancel") { showAddRule = false }
|
||||
}
|
||||
AddMapLocalRuleSheet(rulesRepo: rulesRepo, isPresented: $showAddRule)
|
||||
}
|
||||
.sheet(item: $editingRule) { rule in
|
||||
AddMapLocalRuleSheet(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.deleteMapLocalRule(id: id)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -86,3 +91,155 @@ struct MapLocalView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Add Map Local Rule Sheet
|
||||
|
||||
private struct AddMapLocalRuleSheet: View {
|
||||
let rulesRepo: RulesRepository
|
||||
let existingRule: MapLocalRule?
|
||||
@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 responseStatus = "200"
|
||||
@State private var responseHeadersJSON = ""
|
||||
@State private var responseBody = ""
|
||||
@State private var contentType = "application/json"
|
||||
|
||||
private var methods: [String] { ["ANY"] + ProxyConstants.httpMethods }
|
||||
|
||||
init(rulesRepo: RulesRepository, existingRule: MapLocalRule? = 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("Response") {
|
||||
TextField("Status Code", text: $responseStatus)
|
||||
.keyboardType(.numberPad)
|
||||
TextField("Content Type", text: $contentType)
|
||||
.textInputAutocapitalization(.never)
|
||||
.autocorrectionDisabled()
|
||||
}
|
||||
|
||||
Section {
|
||||
TextEditor(text: $responseHeadersJSON)
|
||||
.font(.system(.body, design: .monospaced))
|
||||
.frame(minHeight: 100)
|
||||
} header: {
|
||||
Text("Response Headers (JSON)")
|
||||
} footer: {
|
||||
Text("Optional JSON object of headers, for example {\"Cache-Control\":\"no-store\",\"X-Mock\":\"1\"}")
|
||||
}
|
||||
|
||||
Section("Response Body") {
|
||||
TextEditor(text: $responseBody)
|
||||
.font(.system(.body, design: .monospaced))
|
||||
.frame(minHeight: 120)
|
||||
}
|
||||
|
||||
Section {
|
||||
Button("Save") {
|
||||
let status = Int(responseStatus) ?? 200
|
||||
if let existing = existingRule {
|
||||
let updated = MapLocalRule(
|
||||
id: existing.id,
|
||||
name: name.isEmpty ? nil : name,
|
||||
urlPattern: urlPattern,
|
||||
method: method,
|
||||
responseStatus: status,
|
||||
responseHeaders: normalizedHeadersJSON,
|
||||
responseBody: responseBody.isEmpty ? nil : responseBody.data(using: .utf8),
|
||||
responseContentType: contentType.isEmpty ? nil : contentType,
|
||||
isEnabled: existing.isEnabled,
|
||||
createdAt: existing.createdAt
|
||||
)
|
||||
try? rulesRepo.updateMapLocalRule(updated)
|
||||
} else {
|
||||
var rule = MapLocalRule(
|
||||
name: name.isEmpty ? nil : name,
|
||||
urlPattern: urlPattern,
|
||||
method: method,
|
||||
responseStatus: status,
|
||||
responseHeaders: normalizedHeadersJSON,
|
||||
responseBody: responseBody.isEmpty ? nil : responseBody.data(using: .utf8),
|
||||
responseContentType: contentType.isEmpty ? nil : contentType
|
||||
)
|
||||
try? rulesRepo.insertMapLocalRule(&rule)
|
||||
}
|
||||
isPresented = false
|
||||
dismiss()
|
||||
}
|
||||
.disabled(urlPattern.isEmpty)
|
||||
}
|
||||
}
|
||||
.navigationTitle(existingRule == nil ? "New Map Local Rule" : "Edit Map Local 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
|
||||
responseStatus = String(rule.responseStatus)
|
||||
if let body = rule.responseBody, let str = String(data: body, encoding: .utf8) {
|
||||
responseBody = str
|
||||
}
|
||||
responseHeadersJSON = prettyPrintedHeaders(rule.responseHeaders) ?? ""
|
||||
contentType = rule.responseContentType ?? "application/json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var normalizedHeadersJSON: String? {
|
||||
let trimmed = responseHeadersJSON.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !trimmed.isEmpty else { return nil }
|
||||
guard let data = trimmed.data(using: .utf8),
|
||||
let object = try? JSONSerialization.jsonObject(with: data),
|
||||
let dict = object as? [String: String],
|
||||
let normalized = try? JSONEncoder().encode(dict),
|
||||
let json = String(data: normalized, encoding: .utf8) else {
|
||||
return nil
|
||||
}
|
||||
return json
|
||||
}
|
||||
|
||||
private func prettyPrintedHeaders(_ json: String?) -> String? {
|
||||
guard let json,
|
||||
let data = json.data(using: .utf8),
|
||||
let object = try? JSONSerialization.jsonObject(with: data),
|
||||
let pretty = try? JSONSerialization.data(withJSONObject: object, options: [.prettyPrinted]),
|
||||
let string = String(data: pretty, encoding: .utf8) else {
|
||||
return json
|
||||
}
|
||||
return string
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user