- 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
203 lines
9.0 KiB
Swift
203 lines
9.0 KiB
Swift
import Foundation
|
|
import GRDB
|
|
import os
|
|
|
|
private let log = Logger(subsystem: "com.treyt.proxyapp", category: "db")
|
|
|
|
public final class DatabaseManager: Sendable {
|
|
public let dbPool: DatabasePool
|
|
|
|
public static let shared: DatabaseManager = {
|
|
let url = AppGroupPaths.containerURL.appendingPathComponent("proxy.sqlite")
|
|
log.info("DB path: \(url.path)")
|
|
do {
|
|
let mgr = try DatabaseManager(path: url.path)
|
|
log.info("DatabaseManager initialized OK")
|
|
return mgr
|
|
} catch {
|
|
log.fault("DatabaseManager FATAL: \(error.localizedDescription)")
|
|
fatalError("[DatabaseManager] Failed to initialize database at \(url.path): \(error)")
|
|
}
|
|
}()
|
|
|
|
public init(path: String) throws {
|
|
var config = Configuration()
|
|
config.prepareDatabase { db in
|
|
// WAL mode for cross-process concurrent access
|
|
try db.execute(sql: "PRAGMA journal_mode = WAL")
|
|
try db.execute(sql: "PRAGMA synchronous = NORMAL")
|
|
}
|
|
dbPool = try DatabasePool(path: path, configuration: config)
|
|
try migrate()
|
|
}
|
|
|
|
private func migrate() throws {
|
|
var migrator = DatabaseMigrator()
|
|
|
|
migrator.registerMigration("v1_create_tables") { db in
|
|
try db.create(table: "captured_traffic") { t in
|
|
t.autoIncrementedPrimaryKey("id")
|
|
t.column("requestId", .text).notNull().unique()
|
|
t.column("domain", .text).notNull().indexed()
|
|
t.column("url", .text).notNull()
|
|
t.column("method", .text).notNull()
|
|
t.column("scheme", .text).notNull()
|
|
t.column("statusCode", .integer)
|
|
t.column("statusText", .text)
|
|
|
|
t.column("requestHeaders", .text)
|
|
t.column("requestBody", .blob)
|
|
t.column("requestBodySize", .integer).notNull().defaults(to: 0)
|
|
t.column("requestContentType", .text)
|
|
t.column("queryParameters", .text)
|
|
|
|
t.column("responseHeaders", .text)
|
|
t.column("responseBody", .blob)
|
|
t.column("responseBodySize", .integer).notNull().defaults(to: 0)
|
|
t.column("responseContentType", .text)
|
|
|
|
t.column("startedAt", .double).notNull()
|
|
t.column("completedAt", .double)
|
|
t.column("durationMs", .integer)
|
|
|
|
t.column("isSslDecrypted", .boolean).notNull().defaults(to: false)
|
|
t.column("isPinned", .boolean).notNull().defaults(to: false)
|
|
t.column("isWebsocket", .boolean).notNull().defaults(to: false)
|
|
t.column("isHidden", .boolean).notNull().defaults(to: false)
|
|
|
|
t.column("createdAt", .double).notNull()
|
|
}
|
|
|
|
try db.create(index: "idx_traffic_started_at", on: "captured_traffic", columns: ["startedAt"])
|
|
try db.create(index: "idx_traffic_pinned", on: "captured_traffic", columns: ["isPinned"])
|
|
|
|
try db.create(table: "ssl_proxying_entries") { t in
|
|
t.autoIncrementedPrimaryKey("id")
|
|
t.column("domainPattern", .text).notNull()
|
|
t.column("isInclude", .boolean).notNull()
|
|
t.column("createdAt", .double).notNull()
|
|
}
|
|
|
|
try db.create(table: "block_list_entries") { t in
|
|
t.autoIncrementedPrimaryKey("id")
|
|
t.column("name", .text)
|
|
t.column("urlPattern", .text).notNull()
|
|
t.column("method", .text).notNull().defaults(to: "ANY")
|
|
t.column("includeSubpaths", .boolean).notNull().defaults(to: true)
|
|
t.column("blockAction", .text).notNull().defaults(to: "block_and_hide")
|
|
t.column("isEnabled", .boolean).notNull().defaults(to: true)
|
|
t.column("createdAt", .double).notNull()
|
|
}
|
|
|
|
try db.create(table: "breakpoint_rules") { t in
|
|
t.autoIncrementedPrimaryKey("id")
|
|
t.column("name", .text)
|
|
t.column("urlPattern", .text).notNull()
|
|
t.column("method", .text).notNull().defaults(to: "ANY")
|
|
t.column("interceptRequest", .boolean).notNull().defaults(to: true)
|
|
t.column("interceptResponse", .boolean).notNull().defaults(to: true)
|
|
t.column("isEnabled", .boolean).notNull().defaults(to: true)
|
|
t.column("createdAt", .double).notNull()
|
|
}
|
|
|
|
try db.create(table: "map_local_rules") { t in
|
|
t.autoIncrementedPrimaryKey("id")
|
|
t.column("name", .text)
|
|
t.column("urlPattern", .text).notNull()
|
|
t.column("method", .text).notNull().defaults(to: "ANY")
|
|
t.column("responseStatus", .integer).notNull().defaults(to: 200)
|
|
t.column("responseHeaders", .text)
|
|
t.column("responseBody", .blob)
|
|
t.column("responseContentType", .text)
|
|
t.column("isEnabled", .boolean).notNull().defaults(to: true)
|
|
t.column("createdAt", .double).notNull()
|
|
}
|
|
|
|
try db.create(table: "dns_spoof_rules") { t in
|
|
t.autoIncrementedPrimaryKey("id")
|
|
t.column("sourceDomain", .text).notNull()
|
|
t.column("targetDomain", .text).notNull()
|
|
t.column("isEnabled", .boolean).notNull().defaults(to: true)
|
|
t.column("createdAt", .double).notNull()
|
|
}
|
|
|
|
try db.create(table: "compose_requests") { t in
|
|
t.autoIncrementedPrimaryKey("id")
|
|
t.column("name", .text).notNull().defaults(to: "New Request")
|
|
t.column("method", .text).notNull().defaults(to: "GET")
|
|
t.column("url", .text)
|
|
t.column("headers", .text)
|
|
t.column("queryParameters", .text)
|
|
t.column("body", .text)
|
|
t.column("bodyContentType", .text)
|
|
t.column("responseStatus", .integer)
|
|
t.column("responseHeaders", .text)
|
|
t.column("responseBody", .blob)
|
|
t.column("lastSentAt", .double)
|
|
t.column("createdAt", .double).notNull()
|
|
}
|
|
}
|
|
|
|
migrator.registerMigration("v2_create_proxy_configuration") { db in
|
|
try db.create(table: "proxy_configuration") { t in
|
|
t.column("id", .integer).primaryKey(onConflict: .replace)
|
|
t.column("sslProxyingEnabled", .boolean).notNull().defaults(to: false)
|
|
t.column("blockListEnabled", .boolean).notNull().defaults(to: false)
|
|
t.column("breakpointEnabled", .boolean).notNull().defaults(to: false)
|
|
t.column("noCachingEnabled", .boolean).notNull().defaults(to: false)
|
|
t.column("dnsSpoofingEnabled", .boolean).notNull().defaults(to: false)
|
|
t.column("hideSystemTraffic", .boolean).notNull().defaults(to: false)
|
|
t.column("updatedAt", .double).notNull()
|
|
}
|
|
|
|
try db.execute(
|
|
sql: """
|
|
INSERT INTO proxy_configuration (
|
|
id, sslProxyingEnabled, blockListEnabled, breakpointEnabled,
|
|
noCachingEnabled, dnsSpoofingEnabled, hideSystemTraffic, updatedAt
|
|
) VALUES (1, 0, 0, 0, 0, 0, 0, ?)
|
|
""",
|
|
arguments: [Date().timeIntervalSince1970]
|
|
)
|
|
}
|
|
|
|
migrator.registerMigration("v3_create_proxy_runtime_status") { db in
|
|
try db.create(table: "proxy_runtime_status") { t in
|
|
t.column("id", .integer).primaryKey(onConflict: .replace)
|
|
t.column("tunnelState", .text).notNull().defaults(to: ProxyRuntimeState.stopped.rawValue)
|
|
t.column("proxyHost", .text)
|
|
t.column("proxyPort", .integer)
|
|
t.column("caFingerprint", .text)
|
|
t.column("lastProxyError", .text)
|
|
t.column("lastMITMError", .text)
|
|
t.column("lastConnectError", .text)
|
|
t.column("lastSuccessfulMITMDomain", .text)
|
|
t.column("lastSuccessfulMITMAt", .double)
|
|
t.column("lastExtensionStartAt", .double)
|
|
t.column("lastExtensionStopAt", .double)
|
|
t.column("updatedAt", .double).notNull()
|
|
}
|
|
|
|
try db.execute(
|
|
sql: """
|
|
INSERT INTO proxy_runtime_status (
|
|
id, tunnelState, updatedAt
|
|
) VALUES (1, ?, ?)
|
|
""",
|
|
arguments: [ProxyRuntimeState.stopped.rawValue, Date().timeIntervalSince1970]
|
|
)
|
|
}
|
|
|
|
migrator.registerMigration("v4_create_pinned_domains") { db in
|
|
try db.create(table: "pinned_domains") { t in
|
|
t.autoIncrementedPrimaryKey("id")
|
|
t.column("domain", .text).notNull().unique()
|
|
t.column("reason", .text)
|
|
t.column("detectedAt", .double).notNull()
|
|
}
|
|
}
|
|
|
|
try migrator.migrate(dbPool)
|
|
}
|
|
}
|