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:
@@ -3,21 +3,30 @@ import ProxyCore
|
||||
|
||||
struct CertificateView: View {
|
||||
@Environment(AppState.self) private var appState
|
||||
@State private var showRegenerateConfirmation = false
|
||||
@State private var isInstallingCert = false
|
||||
@State private var certServer: CertificateInstallServer?
|
||||
|
||||
private var dateFormatter: DateFormatter {
|
||||
let f = DateFormatter()
|
||||
f.dateStyle = .medium
|
||||
return f
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
Section {
|
||||
HStack {
|
||||
Image(systemName: appState.isCertificateTrusted ? "checkmark.shield.fill" : "exclamationmark.shield")
|
||||
Image(systemName: CertificateManager.shared.hasCA ? "checkmark.shield.fill" : "exclamationmark.shield")
|
||||
.font(.largeTitle)
|
||||
.foregroundStyle(appState.isCertificateTrusted ? .green : .orange)
|
||||
.foregroundStyle(CertificateManager.shared.hasCA ? .green : .orange)
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
Text(appState.isCertificateTrusted
|
||||
? "Certificate is installed & trusted!"
|
||||
: "Certificate not installed")
|
||||
Text(CertificateManager.shared.hasCA
|
||||
? "Certificate Generated"
|
||||
: "Certificate not generated")
|
||||
.font(.headline)
|
||||
Text("Required for HTTPS decryption")
|
||||
Text("The app owns the shared CA used for HTTPS decryption.")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
@@ -27,24 +36,173 @@ struct CertificateView: View {
|
||||
|
||||
Section("Details") {
|
||||
LabeledContent("CA Certificate", value: "Proxy CA (\(UIDevice.current.name))")
|
||||
LabeledContent("Generated", value: "-")
|
||||
LabeledContent("Expires", value: "-")
|
||||
LabeledContent("Generated", value: formattedDate(CertificateManager.shared.caGeneratedDate))
|
||||
LabeledContent("Expires", value: formattedDate(CertificateManager.shared.caExpirationDate))
|
||||
LabeledContent("Fingerprint", value: abbreviatedFingerprint(CertificateManager.shared.caFingerprint))
|
||||
}
|
||||
|
||||
Section("Runtime") {
|
||||
LabeledContent("Extension Loaded Same CA", value: appState.hasSharedCertificate ? "Yes" : "No")
|
||||
LabeledContent("HTTPS Inspection Verified", value: appState.isHTTPSInspectionVerified ? "Yes" : "Not Yet")
|
||||
if let domain = appState.runtimeStatus.lastSuccessfulMITMDomain {
|
||||
LabeledContent("Last Verified Domain", value: domain)
|
||||
}
|
||||
if let lastError = appState.lastRuntimeError {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text("Latest Error")
|
||||
.font(.caption.weight(.semibold))
|
||||
Text(lastError)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Section {
|
||||
Button("Install Certificate") {
|
||||
// TODO: Phase 3 - Export and open cert installation
|
||||
Button {
|
||||
installCertificate()
|
||||
} label: {
|
||||
HStack {
|
||||
Spacer()
|
||||
if isInstallingCert {
|
||||
ProgressView()
|
||||
.padding(.trailing, 8)
|
||||
}
|
||||
Text("Install Certificate to Settings")
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.disabled(isInstallingCert || !CertificateManager.shared.hasCA)
|
||||
} footer: {
|
||||
Text("Downloads the CA certificate in Safari. After downloading, install it from Settings > General > VPN & Device Management, then enable trust in Settings > General > About > Certificate Trust Settings.")
|
||||
}
|
||||
|
||||
Section {
|
||||
Button("Regenerate Certificate", role: .destructive) {
|
||||
// TODO: Phase 3 - Generate new CA
|
||||
showRegenerateConfirmation = true
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
.navigationTitle("Certificate")
|
||||
.confirmationDialog("Regenerate Certificate?", isPresented: $showRegenerateConfirmation) {
|
||||
Button("Regenerate", role: .destructive) {
|
||||
CertificateManager.shared.regenerateCA()
|
||||
appState.isCertificateInstalled = CertificateManager.shared.hasCA
|
||||
}
|
||||
} message: {
|
||||
Text("This will create a new CA certificate. You will need to reinstall and trust it on your device.")
|
||||
}
|
||||
.onAppear {
|
||||
appState.isCertificateInstalled = CertificateManager.shared.hasCA
|
||||
}
|
||||
.onDisappear {
|
||||
certServer?.stop()
|
||||
certServer = nil
|
||||
}
|
||||
}
|
||||
|
||||
private func formattedDate(_ date: Date?) -> String {
|
||||
guard let date else { return "N/A" }
|
||||
return dateFormatter.string(from: date)
|
||||
}
|
||||
|
||||
private func abbreviatedFingerprint(_ fingerprint: String?) -> String {
|
||||
guard let fingerprint else { return "N/A" }
|
||||
if fingerprint.count <= 16 { return fingerprint }
|
||||
return "\(fingerprint.prefix(8))...\(fingerprint.suffix(8))"
|
||||
}
|
||||
|
||||
private func installCertificate() {
|
||||
guard let derBytes = CertificateManager.shared.exportCACertificateDER() else { return }
|
||||
|
||||
isInstallingCert = true
|
||||
|
||||
// Start a local HTTP server that serves the certificate
|
||||
let server = CertificateInstallServer(certDER: Data(derBytes))
|
||||
certServer = server
|
||||
|
||||
server.start { port in
|
||||
Task { @MainActor in
|
||||
// Open Safari to our local server so the certificate can be downloaded and installed.
|
||||
if let url = URL(string: "http://localhost:\(port)/ProxyCA.cer") {
|
||||
UIApplication.shared.open(url)
|
||||
}
|
||||
isInstallingCert = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Local HTTP server for certificate installation
|
||||
|
||||
import Network
|
||||
|
||||
final class CertificateInstallServer: @unchecked Sendable {
|
||||
private let certDER: Data
|
||||
private var listener: NWListener?
|
||||
private let queue = DispatchQueue(label: "cert-install-server")
|
||||
|
||||
init(certDER: Data) {
|
||||
self.certDER = certDER
|
||||
}
|
||||
|
||||
func start(onReady: @escaping @Sendable (UInt16) -> Void) {
|
||||
do {
|
||||
let params = NWParameters.tcp
|
||||
listener = try NWListener(using: params, on: .any)
|
||||
|
||||
listener?.stateUpdateHandler = { state in
|
||||
if case .ready = state, let port = self.listener?.port?.rawValue {
|
||||
DispatchQueue.main.async {
|
||||
onReady(port)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
listener?.newConnectionHandler = { [weak self] connection in
|
||||
self?.handleConnection(connection)
|
||||
}
|
||||
|
||||
listener?.start(queue: queue)
|
||||
} catch {
|
||||
print("[CertInstall] Failed to start server: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
func stop() {
|
||||
listener?.cancel()
|
||||
listener = nil
|
||||
}
|
||||
|
||||
private func handleConnection(_ connection: NWConnection) {
|
||||
connection.start(queue: queue)
|
||||
|
||||
// Read the HTTP request (we don't really need to parse it)
|
||||
connection.receive(minimumIncompleteLength: 1, maximumLength: 4096) { [weak self] data, _, _, _ in
|
||||
guard let self else { return }
|
||||
|
||||
// Respond with the certificate as a mobileconfig-style download
|
||||
let body = self.certDER
|
||||
let response = """
|
||||
HTTP/1.1 200 OK\r
|
||||
Content-Type: application/x-x509-ca-cert\r
|
||||
Content-Disposition: attachment; filename="ProxyCA.cer"\r
|
||||
Content-Length: \(body.count)\r
|
||||
Connection: close\r
|
||||
\r\n
|
||||
"""
|
||||
|
||||
var responseData = Data(response.utf8)
|
||||
responseData.append(body)
|
||||
|
||||
connection.send(content: responseData, completion: .contentProcessed { _ in
|
||||
connection.cancel()
|
||||
// Stop the server after serving — one-shot
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
|
||||
self.stop()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user