- 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
101 lines
3.7 KiB
Swift
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)
|
|
}
|
|
}
|
|
}
|