import Foundation /// 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 installiert via `profiles install`. /// Profile-File wird nach Installation gelöscht (enthält sensiblen Token). 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. Install via `profiles` command (macOS-only) let result = try await ProcessRunner.run( "/usr/bin/profiles", arguments: ["install", "-path", profileURL.path] ) // 3. Clean up downloaded file try? FileManager.default.removeItem(at: profileURL) if result.exitCode != 0 { let errorMsg = result.stderr.isEmpty ? result.stdout : result.stderr throw InstallerError.installFailed(errorMsg) } } /// 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") } /// Entfernt ReBreak-DNS-Profile (für Testing/Reset). /// Benötigt PayloadIdentifier — wir suchen nach "org.rebreak.protection.profile.*". 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) } } }