Initial project setup - Phases 1-3 complete

This commit is contained in:
Trey t
2026-04-06 11:28:40 -05:00
commit c77e506db5
293 changed files with 14233 additions and 0 deletions

View File

@@ -0,0 +1,27 @@
import SwiftUI
import ProxyCore
struct AdvancedSettingsView: View {
@State private var hideSystemTraffic = IPCManager.shared.hideSystemTraffic
@State private var showImagePreview = true
var body: some View {
Form {
Section {
Toggle("Hide iOS System Traffic", isOn: $hideSystemTraffic)
.onChange(of: hideSystemTraffic) { _, newValue in
IPCManager.shared.hideSystemTraffic = newValue
}
} footer: {
Text("Hide traffic from Apple system services like push notifications, iCloud sync, and analytics.")
}
Section {
Toggle("Show Image Preview", isOn: $showImagePreview)
} footer: {
Text("Display thumbnail previews for image responses in the traffic list.")
}
}
.navigationTitle("Advanced")
}
}

View File

@@ -0,0 +1,39 @@
import SwiftUI
struct AppSettingsView: View {
@State private var analyticsEnabled = false
@State private var crashReportingEnabled = true
@State private var showClearCacheConfirmation = false
var body: some View {
Form {
Section {
Toggle("Analytics", isOn: $analyticsEnabled)
Toggle("Crash Reporting", isOn: $crashReportingEnabled)
} footer: {
Text("Help improve the app by sharing anonymous usage data.")
}
Section {
Button("Clear App Cache", role: .destructive) {
showClearCacheConfirmation = true
}
} footer: {
Text("Remove all cached data. This does not delete captured traffic.")
}
Section("About") {
LabeledContent("Version", value: "1.0.0")
LabeledContent("Build", value: "1")
}
}
.navigationTitle("App Settings")
.confirmationDialog("Clear Cache", isPresented: $showClearCacheConfirmation) {
Button("Clear Cache", role: .destructive) {
// TODO: Clear URL cache, image cache, etc.
}
} message: {
Text("This will clear all cached data.")
}
}
}

144
UI/More/BlockListView.swift Normal file
View File

@@ -0,0 +1,144 @@
import SwiftUI
import ProxyCore
import GRDB
struct BlockListView: View {
@State private var isEnabled = IPCManager.shared.isBlockListEnabled
@State private var entries: [BlockListEntry] = []
@State private var showAddRule = false
@State private var observation: AnyDatabaseCancellable?
private let rulesRepo = RulesRepository()
var body: some View {
List {
Section {
ToggleHeaderView(
title: "Block List",
description: "Block requests matching these rules. Blocked requests will be dropped or hidden based on the action.",
isEnabled: $isEnabled
)
.onChange(of: isEnabled) { _, newValue in
IPCManager.shared.isBlockListEnabled = newValue
IPCManager.shared.post(.configurationChanged)
}
}
.listRowInsets(EdgeInsets())
Section("Rules") {
if entries.isEmpty {
Text("No block rules")
.foregroundStyle(.secondary)
.font(.subheadline)
} else {
ForEach(entries) { entry in
VStack(alignment: .leading, spacing: 4) {
Text(entry.name ?? entry.urlPattern)
.font(.subheadline.weight(.medium))
Text(entry.urlPattern)
.font(.caption)
.foregroundStyle(.secondary)
Text(entry.action.displayName)
.font(.caption2)
.foregroundStyle(.tertiary)
}
}
.onDelete { indexSet in
for index in indexSet {
if let id = entries[index].id {
try? rulesRepo.deleteBlockEntry(id: id)
}
}
}
}
}
}
.navigationTitle("Block List")
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button { showAddRule = true } label: {
Image(systemName: "plus")
}
}
}
.sheet(isPresented: $showAddRule) {
NewBlockRuleView { entry in
var entry = entry
try? rulesRepo.insertBlockEntry(&entry)
}
}
.task {
observation = rulesRepo.observeBlockListEntries()
.start(in: DatabaseManager.shared.dbPool) { error in
print("Block list observation error: \(error)")
} onChange: { newEntries in
entries = newEntries
}
}
}
}
// MARK: - New Block Rule
struct NewBlockRuleView: View {
let onSave: (BlockListEntry) -> Void
@State private var name = ""
@State private var urlPattern = ""
@State private var method = "ANY"
@State private var includeSubpaths = true
@State private var blockAction: BlockAction = .blockAndHide
@Environment(\.dismiss) private var dismiss
var body: some View {
NavigationStack {
Form {
Section {
TextField("Name (optional)", text: $name)
TextField("URL Pattern", text: $urlPattern)
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
}
Section {
Picker("Method", selection: $method) {
Text("ANY").tag("ANY")
ForEach(ProxyConstants.httpMethods, id: \.self) { m in
Text(m).tag(m)
}
}
Toggle("Include Subpaths", isOn: $includeSubpaths)
}
Section {
Picker("Block Action", selection: $blockAction) {
ForEach(BlockAction.allCases, id: \.self) { action in
Text(action.displayName).tag(action)
}
}
}
}
.navigationTitle("New Block Rule")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Cancel") { dismiss() }
}
ToolbarItem(placement: .confirmationAction) {
Button("Save") {
let entry = BlockListEntry(
name: name.isEmpty ? nil : name,
urlPattern: urlPattern,
method: method,
includeSubpaths: includeSubpaths,
blockAction: blockAction
)
onSave(entry)
dismiss()
}
.disabled(urlPattern.isEmpty)
}
}
}
}
}

View File

@@ -0,0 +1,99 @@
import SwiftUI
import ProxyCore
import GRDB
struct BreakpointRulesView: View {
@State private var isEnabled = IPCManager.shared.isBreakpointEnabled
@State private var rules: [BreakpointRule] = []
@State private var showAddRule = false
@State private var observation: AnyDatabaseCancellable?
private let rulesRepo = RulesRepository()
var body: some View {
List {
Section {
ToggleHeaderView(
title: "Breakpoint",
description: "Pause and modify HTTP requests and responses in real-time before they reach the server or the app.",
isEnabled: $isEnabled
)
.onChange(of: isEnabled) { _, newValue in
IPCManager.shared.isBreakpointEnabled = newValue
IPCManager.shared.post(.configurationChanged)
}
}
.listRowInsets(EdgeInsets())
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
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)
}
}
}
}
}
}
.navigationTitle("Breakpoint")
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button { showAddRule = true } label: {
Image(systemName: "plus")
}
}
}
.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 }
}
}
}
}
.task {
observation = rulesRepo.observeBreakpointRules()
.start(in: DatabaseManager.shared.dbPool) { error in
print("Breakpoint observation error: \(error)")
} onChange: { newRules in
rules = newRules
}
}
}
}

View File

@@ -0,0 +1,50 @@
import SwiftUI
import ProxyCore
struct CertificateView: View {
@Environment(AppState.self) private var appState
var body: some View {
List {
Section {
HStack {
Image(systemName: appState.isCertificateTrusted ? "checkmark.shield.fill" : "exclamationmark.shield")
.font(.largeTitle)
.foregroundStyle(appState.isCertificateTrusted ? .green : .orange)
VStack(alignment: .leading) {
Text(appState.isCertificateTrusted
? "Certificate is installed & trusted!"
: "Certificate not installed")
.font(.headline)
Text("Required for HTTPS decryption")
.font(.caption)
.foregroundStyle(.secondary)
}
}
.padding(.vertical, 8)
}
Section("Details") {
LabeledContent("CA Certificate", value: "Proxy CA (\(UIDevice.current.name))")
LabeledContent("Generated", value: "-")
LabeledContent("Expires", value: "-")
}
Section {
Button("Install Certificate") {
// TODO: Phase 3 - Export and open cert installation
}
.frame(maxWidth: .infinity)
}
Section {
Button("Regenerate Certificate", role: .destructive) {
// TODO: Phase 3 - Generate new CA
}
.frame(maxWidth: .infinity)
}
}
.navigationTitle("Certificate")
}
}

View File

@@ -0,0 +1,92 @@
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 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
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)
}
}
}
.onDelete { indexSet in
for index in indexSet {
if let id = rules[index].id {
try? rulesRepo.deleteDNSSpoofRule(id: id)
}
}
}
}
}
}
.navigationTitle("DNS Spoofing")
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button { showAddRule = true } label: {
Image(systemName: "plus")
}
}
}
.sheet(isPresented: $showAddRule) {
NavigationStack {
Form {
// TODO: Add DNS spoof rule creation form
Text("DNS Spoofing rule creation")
}
.navigationTitle("New DNS Rule")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Cancel") { showAddRule = false }
}
}
}
}
.task {
observation = rulesRepo.observeDNSSpoofRules()
.start(in: DatabaseManager.shared.dbPool) { error in
print("DNS Spoof observation error: \(error)")
} onChange: { newRules in
rules = newRules
}
}
}
}

View File

@@ -0,0 +1,88 @@
import SwiftUI
import ProxyCore
import GRDB
struct MapLocalView: View {
@State private var rules: [MapLocalRule] = []
@State private var showAddRule = false
@State private var observation: AnyDatabaseCancellable?
private let rulesRepo = RulesRepository()
var body: some View {
List {
Section {
VStack(alignment: .leading, spacing: 8) {
Text("Map Local")
.font(.headline)
Text("Intercept requests and replace the response with local content. Define custom mock responses for matched URLs.")
.font(.subheadline)
.foregroundStyle(.secondary)
}
.padding()
}
.listRowInsets(EdgeInsets())
Section("Rules") {
if rules.isEmpty {
EmptyStateView(
icon: "doc.on.doc",
title: "No Map Local Rules",
subtitle: "Tap + to create a new rule."
)
} 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)
}
}
.onDelete { indexSet in
for index in indexSet {
if let id = rules[index].id {
try? rulesRepo.deleteMapLocalRule(id: id)
}
}
}
}
}
}
.navigationTitle("Map Local")
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button { showAddRule = true } label: {
Image(systemName: "plus")
}
}
}
.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 }
}
}
}
}
.task {
observation = rulesRepo.observeMapLocalRules()
.start(in: DatabaseManager.shared.dbPool) { error in
print("Map Local observation error: \(error)")
} onChange: { newRules in
rules = newRules
}
}
}
}

87
UI/More/MoreView.swift Normal file
View File

@@ -0,0 +1,87 @@
import SwiftUI
import ProxyCore
struct MoreView: View {
@Environment(AppState.self) private var appState
var body: some View {
List {
Section {
NavigationLink {
SetupGuideView()
} label: {
Label {
VStack(alignment: .leading) {
Text("Setup Guide")
Text(appState.isVPNConnected ? "Ready to Intercept" : "Setup Required")
.font(.caption)
.foregroundStyle(appState.isVPNConnected ? .green : .orange)
}
} icon: {
Image(systemName: "checkmark.shield.fill")
.foregroundStyle(appState.isVPNConnected ? .green : .orange)
}
}
NavigationLink {
CertificateView()
} label: {
Label("Certificate", systemImage: "lock.shield")
}
}
Section("Rules") {
NavigationLink {
SSLProxyingListView()
} label: {
Label("SSL Proxying List", systemImage: "lock.fill")
}
NavigationLink {
BlockListView()
} label: {
Label("Block List", systemImage: "xmark.shield")
}
NavigationLink {
BreakpointRulesView()
} label: {
Label("Breakpoint", systemImage: "pause.circle")
}
NavigationLink {
MapLocalView()
} label: {
Label("Map Local", systemImage: "doc.on.doc")
}
NavigationLink {
NoCachingView()
} label: {
Label("No Caching", systemImage: "arrow.clockwise.circle")
}
NavigationLink {
DNSSpoofingView()
} label: {
Label("DNS Spoofing", systemImage: "network")
}
}
Section("Settings") {
NavigationLink {
AdvancedSettingsView()
} label: {
Label("Advanced", systemImage: "gearshape.2")
}
NavigationLink {
AppSettingsView()
} label: {
Label("App Settings", systemImage: "gearshape")
}
}
}
.navigationTitle("More")
}
}

View File

@@ -0,0 +1,42 @@
import SwiftUI
import ProxyCore
struct NoCachingView: View {
@State private var isEnabled = IPCManager.shared.isNoCachingEnabled
var body: some View {
List {
Section {
ToggleHeaderView(
title: "No Caching",
description: "Bypass all caching layers to always see the latest server response. Strips cache headers from requests and responses.",
isEnabled: $isEnabled
)
.onChange(of: isEnabled) { _, newValue in
IPCManager.shared.isNoCachingEnabled = newValue
IPCManager.shared.post(.configurationChanged)
}
}
.listRowInsets(EdgeInsets())
Section("How it works") {
VStack(alignment: .leading, spacing: 8) {
Text("Request modifications:")
.font(.caption.weight(.semibold))
Text("Removes If-Modified-Since, If-None-Match\nAdds Pragma: no-cache, Cache-Control: no-cache")
.font(.caption)
.foregroundStyle(.secondary)
}
VStack(alignment: .leading, spacing: 8) {
Text("Response modifications:")
.font(.caption.weight(.semibold))
Text("Removes Expires, Last-Modified, ETag\nAdds Expires: 0, Cache-Control: no-cache")
.font(.caption)
.foregroundStyle(.secondary)
}
}
}
.navigationTitle("No Caching")
}
}

View File

@@ -0,0 +1,150 @@
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)
}
}
}
}
}

View File

@@ -0,0 +1,116 @@
import SwiftUI
import ProxyCore
struct SetupGuideView: View {
@Environment(AppState.self) private var appState
var isReady: Bool {
appState.isVPNConnected && appState.isCertificateTrusted
}
var body: some View {
VStack(spacing: 24) {
// Status Banner
HStack {
Image(systemName: isReady ? "checkmark.circle.fill" : "exclamationmark.triangle.fill")
.font(.title2)
VStack(alignment: .leading) {
Text(isReady ? "Ready to Intercept" : "Setup Required")
.font(.headline)
Text(isReady ? "All systems are configured correctly" : "Complete the steps below to start")
.font(.caption)
.foregroundStyle(.white.opacity(0.8))
}
Spacer()
}
.foregroundStyle(.white)
.padding()
.background(isReady ? Color.green : Color.orange, in: RoundedRectangle(cornerRadius: 12))
Text("Follow these two steps to start capturing network traffic on your device.")
.font(.subheadline)
.foregroundStyle(.secondary)
// Step 1: VPN
stepRow(
title: "VPN Extension Enabled",
subtitle: appState.isVPNConnected
? "VPN is running and capturing traffic"
: "Tap to enable VPN",
isComplete: appState.isVPNConnected,
action: {
Task { await appState.toggleVPN() }
}
)
// Step 2: Certificate
stepRow(
title: "Certificate Installed & Trusted",
subtitle: appState.isCertificateTrusted
? "HTTPS traffic can now be decrypted"
: "Install and trust the CA certificate",
isComplete: appState.isCertificateTrusted,
action: {
// TODO: Phase 3 - Open certificate installation flow
}
)
Divider()
// Help section
VStack(alignment: .leading, spacing: 12) {
Text("Need Help?")
.font(.headline)
HStack {
Image(systemName: "play.rectangle.fill")
.foregroundStyle(.red)
Text("Watch Video Tutorial")
Spacer()
Image(systemName: "arrow.up.forward")
.foregroundStyle(.secondary)
}
.padding(.vertical, 4)
HStack {
Image(systemName: "book.fill")
.foregroundStyle(.blue)
Text("Read Documentation")
Spacer()
Image(systemName: "arrow.up.forward")
.foregroundStyle(.secondary)
}
.padding(.vertical, 4)
}
Spacer()
}
.padding()
.navigationTitle("Setup Guide")
.navigationBarTitleDisplayMode(.inline)
}
private func stepRow(title: String, subtitle: String, isComplete: Bool, action: @escaping () -> Void) -> some View {
Button(action: action) {
HStack(spacing: 12) {
Image(systemName: isComplete ? "checkmark.circle.fill" : "circle")
.font(.title2)
.foregroundStyle(isComplete ? .green : .secondary)
VStack(alignment: .leading) {
Text(title)
.font(.subheadline.weight(.medium))
.foregroundStyle(isComplete ? .green : .primary)
Text(subtitle)
.font(.caption)
.foregroundStyle(.secondary)
}
Spacer()
}
.padding()
.background(Color(.secondarySystemGroupedBackground), in: RoundedRectangle(cornerRadius: 12))
}
.buttonStyle(.plain)
}
}