import Foundation import AppKit /// Service für Mac-DNS-Profile-Download + Installation. enum MacProfileInstaller { enum InstallerError: Error, LocalizedError { case noRegistration case downloadFailed(String) case installFailed(String) var errorDescription: String? { switch self { case .noRegistration: return "Mac ist nicht registriert. Bitte zuerst registrieren." case .downloadFailed(let msg): return "Profile-Download fehlgeschlagen: \(msg)" case .installFailed(let msg): return "Profile-Installation fehlgeschlagen: \(msg)" } } } /// Lädt Mac-DNS-Profile von Backend und öffnet es in System Settings → Profiles. /// Ab macOS 15+ ist `profiles install` für Configuration Profiles entfernt /// ("profiles tool no longer supports installs. Use System Settings /// Profiles to add configuration profiles."). Einzig zulässiger Weg ohne /// MDM-Enrollment: NSWorkspace öffnet die .mobileconfig → Profiles-Pane /// erscheint → User muss manuell „Installieren" klicken + Admin-PW geben. static func downloadAndInstall(registration: MagicRegistration) async throws { // 1. Download profile let profileURL: URL do { profileURL = try await MagicAPIClient.shared.downloadProfile(token: registration.dnsToken) } catch { throw InstallerError.downloadFailed(error.localizedDescription) } // 2. Open in System Settings → Profiles (user must confirm in UI) let opened = await MainActor.run { NSWorkspace.shared.open(profileURL) } if !opened { try? FileManager.default.removeItem(at: profileURL) throw InstallerError.installFailed( "System Settings konnte das Profil nicht öffnen. Datei liegt unter: \(profileURL.path)" ) } // NICHT löschen — System Settings braucht die Datei evtl. noch. // OS räumt /tmp selbst auf. } /// Prüft ob ReBreak-DNS-Profile bereits installiert ist. /// Verwendet `profiles show -type configuration`. static func isInstalled() async -> Bool { guard let result = try? await ProcessRunner.run( "/usr/bin/profiles", arguments: ["show", "-type", "configuration"] ), result.exitCode == 0 else { return false } // Suche nach PayloadIdentifier pattern org.rebreak.protection.dns.filter* return result.stdout.localizedCaseInsensitiveContains("org.rebreak.protection.dns.filter") || result.stdout.localizedCaseInsensitiveContains("org.rebreak.protection.profile") } // MARK: - Debug only #if DEBUG /// Entfernt ReBreak-DNS-Profile. /// NUR in Debug-Builds verfügbar — darf nicht im Release-Binary landen. static func remove() async throws { // 1. Find identifier guard let result = try? await ProcessRunner.run( "/usr/bin/profiles", arguments: ["show", "-type", "configuration"] ), result.exitCode == 0 else { return } // Parse identifier aus Output (format: " : ") let lines = result.stdout.split(separator: "\n") var identifier: String? for line in lines { let trimmed = line.trimmingCharacters(in: .whitespaces) if trimmed.hasPrefix("org.rebreak.protection.profile") { // Format: "org.rebreak.protection.profile.abc123: ReBreak Protection" identifier = trimmed.split(separator: ":").first.map(String.init) break } } guard let id = identifier else { return } // 2. Remove profile let removeResult = try await ProcessRunner.run( "/usr/bin/profiles", arguments: ["remove", "-identifier", id] ) if removeResult.exitCode != 0 { throw InstallerError.installFailed(removeResult.stderr) } } #endif }