chahinebrini c1edef8abd feat(magic): RebreakMagic device-binding + DNS profile
- backend: /api/magic/{register,devices,profile,release} + AdGuard provisioning + 24h cooldown
- prisma: magic_binding_fields migration (additive on UserDevice)
- mac-app: Phase 2 - Login + MacRegistration + Profile install
- marketing: landing section + /download/rebreakmagic + DMG
- lyra: forbidden phrases + RebreakMagic coach guidance
2026-06-02 09:15:19 +02:00

101 lines
3.7 KiB
Swift

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: " <identifier>: <displayName>")
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)
}
}
}