diff --git a/Mist.xcodeproj/project.pbxproj b/Mist.xcodeproj/project.pbxproj index 342ee08..98c6920 100644 --- a/Mist.xcodeproj/project.pbxproj +++ b/Mist.xcodeproj/project.pbxproj @@ -94,6 +94,7 @@ 398734D028603D9E00B4C357 /* UInt8+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 398734CF28603D9E00B4C357 /* UInt8+Extension.swift */; }; 398734D228603DE700B4C357 /* Array+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 398734D128603DE700B4C357 /* Array+Extension.swift */; }; 398734D4286046B000B4C357 /* UInt32+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 398734D3286046B000B4C357 /* UInt32+Extension.swift */; }; + 39CA25E32941D8BB0030711E /* FileAttributesUpdater.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39CA25E22941D8BB0030711E /* FileAttributesUpdater.swift */; }; 39CB5E3D293F5C2E00CFDBB8 /* Catalog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39CB5E3C293F5C2E00CFDBB8 /* Catalog.swift */; }; 39CB5E3F2941486D00CFDBB8 /* CatalogSeedType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39CB5E3E2941486D00CFDBB8 /* CatalogSeedType.swift */; }; 39CB5E5429418A2900CFDBB8 /* MistTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39CB5E5329418A2900CFDBB8 /* MistTests.swift */; }; @@ -239,6 +240,7 @@ 398734CF28603D9E00B4C357 /* UInt8+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UInt8+Extension.swift"; sourceTree = ""; }; 398734D128603DE700B4C357 /* Array+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Extension.swift"; sourceTree = ""; }; 398734D3286046B000B4C357 /* UInt32+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UInt32+Extension.swift"; sourceTree = ""; }; + 39CA25E22941D8BB0030711E /* FileAttributesUpdater.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileAttributesUpdater.swift; sourceTree = ""; }; 39CB5E3C293F5C2E00CFDBB8 /* Catalog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Catalog.swift; sourceTree = ""; }; 39CB5E3E2941486D00CFDBB8 /* CatalogSeedType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CatalogSeedType.swift; sourceTree = ""; }; 39CB5E5129418A2900CFDBB8 /* MistTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MistTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -373,6 +375,7 @@ 39CF56202861C992006FB5D2 /* DiskImageMounter.swift */, 39CF56232861CA85006FB5D2 /* DiskImageUnmounter.swift */, 3935F47D2864813B00760AB0 /* DownloadManager.swift */, + 39CA25E22941D8BB0030711E /* FileAttributesUpdater.swift */, 39CF56382862D75D006FB5D2 /* FileCreator.swift */, 39CF56342862D4BF006FB5D2 /* FileCompressor.swift */, 39CF56162861BE66006FB5D2 /* FileCopier.swift */, @@ -803,6 +806,7 @@ 390451D02856F63700E0B563 /* Installer.swift in Sources */, 3935F47628643AF000760AB0 /* UNNotificationAction+Extension.swift in Sources */, 39252AB3285C5D7700956C74 /* SettingsGeneralUpdatesView.swift in Sources */, + 39CA25E32941D8BB0030711E /* FileAttributesUpdater.swift in Sources */, 3935F4AB286B04BC00760AB0 /* HelperToolInfoPropertyList.swift in Sources */, 393F35BC28641181005B7165 /* RefreshState.swift in Sources */, 390451CA2856F1D300E0B563 /* ScaledImage.swift in Sources */, diff --git a/Mist/Helpers/FileAttributesUpdater.swift b/Mist/Helpers/FileAttributesUpdater.swift new file mode 100644 index 0000000..63bd36f --- /dev/null +++ b/Mist/Helpers/FileAttributesUpdater.swift @@ -0,0 +1,36 @@ +// +// FileAttributesUpdater.swift +// Mist +// +// Created by Nindi Gill on 8/12/2022. +// + +import Foundation +import SecureXPC + +/// Helper struct to update file / directory attributes +struct FileAttributesUpdater { + + /// Update file / directory attributes at the provided URL. + /// + /// - Parameters: + /// - url: The URL of the file / directory to update. + /// - ownerAccountName: The username of the user that will be used to set the file / directory ownership. + /// + /// - Throws: An `Error` if the command failed to execute. + static func update(url: URL, ownerAccountName: String) async throws { + + guard FileManager.default.fileExists(atPath: url.path) else { + return + } + + let arguments: [String] = [url.path, ownerAccountName] + let client: XPCClient = XPCClient.forMachService(named: .helperIdentifier) + let request: HelperToolCommandRequest = HelperToolCommandRequest(type: .fileAttributes, arguments: arguments, environment: [:]) + let response: HelperToolCommandResponse = try await client.sendMessage(request, to: XPCRoute.commandRoute) + + guard response.terminationStatus == 0 else { + throw MistError.invalidTerminationStatus(status: response.terminationStatus, string: response.standardError) + } + } +} diff --git a/Mist/Helpers/TaskManager.swift b/Mist/Helpers/TaskManager.swift index 21027ae..b5305d3 100644 --- a/Mist/Helpers/TaskManager.swift +++ b/Mist/Helpers/TaskManager.swift @@ -6,6 +6,7 @@ // import Foundation +import System // swiftlint:disable file_length // swiftlint:disable:next type_body_length @@ -231,6 +232,24 @@ class TaskManager: ObservableObject { try await DirectoryCreator.create(cacheDirectoryURL, withIntermediateDirectories: true) } ] + } else { + let attributes: [FileAttributeKey: Any] = try FileManager.default.attributesOfItem(atPath: cacheDirectoryURL.path) + + guard let posixPermissions: NSNumber = attributes[.posixPermissions] as? NSNumber, + let ownerAccountName: String = attributes[.ownerAccountName] as? String, + let groupOwnerAccountName: String = attributes[.groupOwnerAccountName] as? String else { + throw MistError.missingFileAttributes + } + + let filePermissions: FilePermissions = FilePermissions(rawValue: CModeT(posixPermissions.int16Value)) + + if filePermissions != [.ownerReadWriteExecute, .groupReadExecute, .otherReadExecute] || ownerAccountName != NSUserName() || groupOwnerAccountName != "staff" { + tasks += [ + MistTask(type: .configure, description: "cache directory") { + try await FileAttributesUpdater.update(url: cacheDirectoryURL, ownerAccountName: ownerAccountName) + } + ] + } } for package in installer.allDownloads { diff --git a/Mist/Model/DownloadAlertType.swift b/Mist/Model/DownloadAlertType.swift index bfa0703..c8b35a1 100644 --- a/Mist/Model/DownloadAlertType.swift +++ b/Mist/Model/DownloadAlertType.swift @@ -10,4 +10,5 @@ import Foundation enum DownloadAlertType: String { case compatibility = "Compatiblity" case helperTool = "Helper Tool" + case cacheDirectory = "Cache Directory" } diff --git a/Mist/Model/MistError.swift b/Mist/Model/MistError.swift index 1f688de..9b9c558 100644 --- a/Mist/Model/MistError.swift +++ b/Mist/Model/MistError.swift @@ -19,6 +19,7 @@ enum MistError: Error, Equatable { case invalidTerminationStatus(status: Int32, string: String?) case invalidURL(_ url: String) case maximumRetriesReached + case missingFileAttributes case outputStreamBufferError case outputStreamWriteError case userCancelled @@ -51,6 +52,8 @@ enum MistError: Error, Equatable { return "Invalid URL: '\(url)'" case .maximumRetriesReached: return "Maximum number of retries reached" + case .missingFileAttributes: + return "Missing file attributes" case .outputStreamBufferError: return "Output Stream Buffer Error" case .outputStreamWriteError: diff --git a/Mist/Views/List/ListRow.swift b/Mist/Views/List/ListRow.swift index 1ee4751..4374d2d 100644 --- a/Mist/Views/List/ListRow.swift +++ b/Mist/Views/List/ListRow.swift @@ -7,6 +7,7 @@ import Blessed import SwiftUI +import System struct ListRow: View { var type: DownloadType @@ -21,6 +22,8 @@ struct ListRow: View { @ObservedObject var taskManager: TaskManager @State private var alertType: DownloadAlertType = .compatibility @State private var showAlert: Bool = false + @AppStorage("cacheDownloads") private var cacheDownloads: Bool = false + @AppStorage("cacheDirectory") private var cacheDirectory: String = .cacheDirectory private let length: CGFloat = 48 private let spacing: CGFloat = 5 private var compatibilityTitle: String { @@ -43,6 +46,12 @@ struct ListRow: View { private var privilegedHelperToolMessage: String { "The Mist Privileged Helper Tool is required to perform Administrator tasks when \(type == .firmware ? "downloading macOS Firmwares" : "creating macOS Installers")." } + private var cacheDirectoryTitle: String { + "Cache directory settings incorrect!" + } + private var cacheDirectoryMessage: String { + "The cache directory has incorrect ownership and/or permissions, which will cause issues caching macOS Installers.\n\nRepair the cache directory ownership and/or permissions and try again." + } var body: some View { HStack { @@ -83,7 +92,14 @@ struct ListRow: View { return Alert( title: Text(privilegedHelperToolTitle), message: Text(privilegedHelperToolMessage), - primaryButton: .default(Text("Install...")) { install() }, + primaryButton: .default(Text("Install...")) { installPrivilegedHelperTool() }, + secondaryButton: .default(Text("Cancel")) + ) + case .cacheDirectory: + return Alert( + title: Text(cacheDirectoryTitle), + message: Text(cacheDirectoryMessage), + primaryButton: .default(Text("Repair...")) { Task { try await repairCacheDirectoryOwnershipAndPermissions() } }, secondaryButton: .default(Text("Cancel")) ) } @@ -103,12 +119,47 @@ struct ListRow: View { return } + if cacheDownloads { + + do { + let attributes: [FileAttributeKey: Any] = try FileManager.default.attributesOfItem(atPath: cacheDirectory) + + guard let posixPermissions: NSNumber = attributes[.posixPermissions] as? NSNumber else { + alertType = .cacheDirectory + showAlert = true + return + } + + let filePermissions: FilePermissions = FilePermissions(rawValue: CModeT(posixPermissions.int16Value)) + + guard filePermissions == [.ownerReadWriteExecute, .groupReadExecute, .otherReadExecute], + let ownerAccountName: String = attributes[.ownerAccountName] as? String, + ownerAccountName == NSUserName(), + let groupOwnerAccountName: String = attributes[.groupOwnerAccountName] as? String, + groupOwnerAccountName == "staff" else { + alertType = .cacheDirectory + showAlert = true + return + } + } catch { + alertType = .cacheDirectory + showAlert = true + return + } + } + showPanel = true } - private func install() { + private func installPrivilegedHelperTool() { try? PrivilegedHelperManager.shared.authorizeAndBless() } + + private func repairCacheDirectoryOwnershipAndPermissions() async throws { + let url: URL = URL(fileURLWithPath: cacheDirectory) + let ownerAccountName: String = NSUserName() + try await FileAttributesUpdater.update(url: url, ownerAccountName: ownerAccountName) + } } struct ListRow_Previews: PreviewProvider { diff --git a/MistHelperTool/Info.plist b/MistHelperTool/Info.plist index 0296bb9..60b50b3 100644 --- a/MistHelperTool/Info.plist +++ b/MistHelperTool/Info.plist @@ -3,7 +3,7 @@ BuildHash - 631381272a839c41efa70afa56e8d6e05a0bdbc167d07ecae792ed338ac0a353 + 977665398f7c4fc4f84fda51c877a98d25ab4a8ed94c7c532dc7070c8e1c845d CFBundleIdentifier com.ninxsoft.mist.helper CFBundleInfoDictionaryVersion diff --git a/MistHelperTool/main.swift b/MistHelperTool/main.swift index fd34725..5edbc7a 100644 --- a/MistHelperTool/main.swift +++ b/MistHelperTool/main.swift @@ -25,7 +25,7 @@ struct HelperToolCommandRunner { case .remove: guard let path: String = request.arguments.first else { - return HelperToolCommandResponse(terminationStatus: 1, standardOutput: nil, standardError: "Invalid URL") + return HelperToolCommandResponse(terminationStatus: 1, standardOutput: nil, standardError: "Invalid URL: \(request.arguments)") } guard FileManager.default.fileExists(atPath: path) else { @@ -38,6 +38,25 @@ struct HelperToolCommandRunner { } catch { return HelperToolCommandResponse(terminationStatus: 1, standardOutput: nil, standardError: error.localizedDescription) } + case .fileAttributes: + + guard let path: String = request.arguments.first, + let ownerAccountName: String = request.arguments.last else { + return HelperToolCommandResponse(terminationStatus: 1, standardOutput: nil, standardError: "Invalid attributes: \(request.arguments)") + } + + let attributes: [FileAttributeKey: Any] = [ + .posixPermissions: 0o755, + .ownerAccountName: ownerAccountName, + .groupOwnerAccountName: "staff" + ] + + do { + try FileManager.default.setAttributes(attributes, ofItemAtPath: path) + return HelperToolCommandResponse(terminationStatus: 0, standardOutput: nil, standardError: nil) + } catch { + return HelperToolCommandResponse(terminationStatus: 1, standardOutput: nil, standardError: error.localizedDescription) + } case .kill: ShellExecutor.shared.terminate() return HelperToolCommandResponse(terminationStatus: 0, standardOutput: nil, standardError: nil) diff --git a/Shared/HelperToolCommandType.swift b/Shared/HelperToolCommandType.swift index 566547c..94875d9 100644 --- a/Shared/HelperToolCommandType.swift +++ b/Shared/HelperToolCommandType.swift @@ -11,6 +11,8 @@ enum HelperToolCommandType: String, Codable { // swiftlint:disable:next redundant_string_enum_value case remove = "remove" // swiftlint:disable:next redundant_string_enum_value + case fileAttributes = "fileAttributes" + // swiftlint:disable:next redundant_string_enum_value case installer = "installer" // swiftlint:disable:next redundant_string_enum_value case createinstallmedia = "createinstallmedia" diff --git a/Shared/ShellExecutor.swift b/Shared/ShellExecutor.swift index 3b17bc1..1b8c0c6 100644 --- a/Shared/ShellExecutor.swift +++ b/Shared/ShellExecutor.swift @@ -59,8 +59,6 @@ class ShellExecutor: NSObject { return (terminationStatus: terminationStatus, standardOutput: standardOutput, standardError: (standardError ?? "").isEmpty ? nil : standardError) } - // swiftlint:enable large_tuple - func terminate() { guard process.isRunning else {