Files
puter/Sources/puter-helper/main.swift
T
soconnor 0da94715be
Build and test / macos (push) Canceled after 0s
Signed release / release (push) Canceled after 0s
Add advanced monitoring and release infrastructure
2026-08-15 18:23:35 -04:00

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()