115 lines
4.8 KiB
Swift
115 lines
4.8 KiB
Swift
import Foundation
|
|
import PuterHelperProtocol
|
|
import Security
|
|
|
|
private final class HelperService: NSObject, PuterPrivilegedHelperProtocol {
|
|
func version(withReply reply: @escaping (Int) -> Void) {
|
|
reply(PuterHelperConstants.protocolVersion)
|
|
}
|
|
|
|
func setFanTargets(_ targets: [NSNumber: NSNumber], withReply reply: @escaping (Bool, String?) -> Void) {
|
|
guard let validated = PuterFanTargetValidator.validate(targets) else {
|
|
reply(false, "Fan targets failed validation.")
|
|
return
|
|
}
|
|
do {
|
|
for (fan, rpm) in validated {
|
|
try runSMC(["fan", "\(fan)", "-m", "1"])
|
|
try runSMC(["fan", "\(fan)", "-v", "\(rpm)"])
|
|
}
|
|
reply(true, nil)
|
|
} catch { reply(false, error.localizedDescription) }
|
|
}
|
|
|
|
func restoreAutomaticFanControl(withReply reply: @escaping (Bool, String?) -> Void) {
|
|
do {
|
|
try runSMC(["reset"])
|
|
reply(true, nil)
|
|
} catch { reply(false, error.localizedDescription) }
|
|
}
|
|
|
|
private func runSMC(_ arguments: [String]) throws {
|
|
let helperURL = URL(fileURLWithPath: CommandLine.arguments[0]).standardizedFileURL
|
|
let toolURL = helperURL.deletingLastPathComponent().appendingPathComponent("smc")
|
|
guard FileManager.default.isExecutableFile(atPath: toolURL.path) else {
|
|
throw HelperError.smcUnavailable
|
|
}
|
|
let process = Process()
|
|
let errorPipe = Pipe()
|
|
process.executableURL = toolURL
|
|
process.arguments = arguments
|
|
process.standardOutput = FileHandle.nullDevice
|
|
process.standardError = errorPipe
|
|
try process.run()
|
|
let errorData = errorPipe.fileHandleForReading.readDataToEndOfFile()
|
|
process.waitUntilExit()
|
|
guard process.terminationStatus == 0 else {
|
|
throw HelperError.commandFailed(String(decoding: errorData, as: UTF8.self).trimmingCharacters(in: .whitespacesAndNewlines))
|
|
}
|
|
}
|
|
}
|
|
|
|
private enum HelperError: LocalizedError {
|
|
case smcUnavailable
|
|
case commandFailed(String)
|
|
var errorDescription: String? {
|
|
switch self {
|
|
case .smcUnavailable: "The bundled SMC backend is unavailable."
|
|
case .commandFailed(let detail): detail.isEmpty ? "The SMC command failed." : detail
|
|
}
|
|
}
|
|
}
|
|
|
|
private final class HelperDelegate: NSObject, NSXPCListenerDelegate {
|
|
private let service = HelperService()
|
|
|
|
func listener(_ listener: NSXPCListener, shouldAcceptNewConnection connection: NSXPCConnection) -> Bool {
|
|
guard ClientCodeValidator.isTrusted(pid: connection.processIdentifier) else { return false }
|
|
connection.exportedInterface = NSXPCInterface(with: PuterPrivilegedHelperProtocol.self)
|
|
connection.exportedObject = service
|
|
connection.resume()
|
|
return true
|
|
}
|
|
}
|
|
|
|
private enum ClientCodeValidator {
|
|
static func isTrusted(pid: pid_t) -> Bool {
|
|
var clientCode: SecCode?
|
|
let attributes = [kSecGuestAttributePid: NSNumber(value: pid)] as CFDictionary
|
|
guard SecCodeCopyGuestWithAttributes(nil, attributes, [], &clientCode) == errSecSuccess,
|
|
let clientCode,
|
|
let clientInfo = signingInfo(for: clientCode),
|
|
clientInfo.identifier == "dev.soconnor.puter" else { return false }
|
|
|
|
var helperCode: SecCode?
|
|
guard SecCodeCopySelf([], &helperCode) == errSecSuccess,
|
|
let helperCode,
|
|
let helperInfo = signingInfo(for: helperCode),
|
|
let clientTeam = clientInfo.team,
|
|
let helperTeam = helperInfo.team,
|
|
clientTeam == helperTeam else { return false }
|
|
|
|
var requirement: SecRequirement?
|
|
let text = "anchor apple generic and identifier \"dev.soconnor.puter\""
|
|
guard SecRequirementCreateWithString(text as CFString, [], &requirement) == errSecSuccess,
|
|
let requirement else { return false }
|
|
return SecCodeCheckValidity(clientCode, [], requirement) == errSecSuccess
|
|
}
|
|
|
|
private static func signingInfo(for code: SecCode) -> (identifier: String, team: String?)? {
|
|
var staticCode: SecStaticCode?
|
|
guard SecCodeCopyStaticCode(code, [], &staticCode) == errSecSuccess, let staticCode else { return nil }
|
|
var information: CFDictionary?
|
|
guard SecCodeCopySigningInformation(staticCode, SecCSFlags(rawValue: kSecCSSigningInformation), &information) == errSecSuccess,
|
|
let values = information as? [CFString: Any],
|
|
let identifier = values[kSecCodeInfoIdentifier] as? String else { return nil }
|
|
return (identifier, values[kSecCodeInfoTeamIdentifier] as? String)
|
|
}
|
|
}
|
|
|
|
private let delegate = HelperDelegate()
|
|
private let listener = NSXPCListener(machServiceName: PuterHelperConstants.machServiceName)
|
|
listener.delegate = delegate
|
|
listener.resume()
|
|
RunLoop.current.run()
|