Files
Adobe-Downloader/AdobeDownloaderHelperTool/main.swift

307 lines
11 KiB
Swift
Raw Normal View History

import Foundation
import os.log
2025-01-14 17:25:08 +08:00
@objc enum CommandType: Int {
case install
case uninstall
case moveFile
case setPermissions
case shellCommand
}
@objc(HelperToolProtocol) protocol HelperToolProtocol {
@objc(executeCommand:path1:path2:permissions:withReply:)
func executeCommand(type: CommandType, path1: String, path2: String, permissions: Int, withReply reply: @escaping (String) -> Void)
func getInstallationOutput(withReply reply: @escaping (String) -> Void)
}
2025-01-14 17:25:08 +08:00
class SecureCommandHandler {
static func createCommand(type: CommandType, path1: String, path2: String = "", permissions: Int = 0) -> String? {
if type == .shellCommand {
return path1
}
if type != .shellCommand {
if !validatePath(path1) || (!path2.isEmpty && !validatePath(path2)) {
return nil
}
}
switch type {
case .install:
return "installer -pkg \"\(path1)\" -target /"
case .uninstall:
if path1.contains("*") {
return "rm -rf \(path1)"
}
if path1.hasPrefix("\"") && path1.hasSuffix("\"") {
return "rm -rf \(path1)"
}
2025-01-14 17:25:08 +08:00
return "rm -rf \"\(path1)\""
case .moveFile:
let source = path1.hasPrefix("\"") ? path1 : "\"\(path1)\""
let dest = path2.hasPrefix("\"") ? path2 : "\"\(path2)\""
return "cp \(source) \(dest)"
2025-01-14 17:25:08 +08:00
case .setPermissions:
return "chmod \(permissions) \"\(path1)\""
case .shellCommand:
return path1
}
}
static func validatePath(_ path: String) -> Bool {
let cleanPath = path.trimmingCharacters(in: .init(charactersIn: "\"'"))
2025-01-14 17:25:08 +08:00
let allowedPaths = ["/Library/Application Support/Adobe"]
if allowedPaths.contains(where: { cleanPath.hasPrefix($0) }) {
2025-01-14 17:25:08 +08:00
return true
}
let forbiddenPaths = ["/System", "/usr", "/bin", "/sbin", "/var"]
return !forbiddenPaths.contains { cleanPath.hasPrefix($0) }
2025-01-14 17:25:08 +08:00
}
}
2024-11-13 13:20:25 +08:00
class HelperTool: NSObject, HelperToolProtocol {
private let listener: NSXPCListener
private var connections: Set<NSXPCConnection> = []
private var currentTask: Process?
private var outputPipe: Pipe?
2025-01-14 17:25:08 +08:00
private var outputBuffer: String = ""
private let logger = Logger(subsystem: "com.x1a0he.macOS.Adobe-Downloader.helper", category: "Helper")
2025-01-14 17:25:08 +08:00
private let operationQueue = DispatchQueue(label: "com.x1a0he.macOS.Adobe-Downloader.helper.operation")
2024-11-13 13:20:25 +08:00
override init() {
listener = NSXPCListener(machServiceName: "com.x1a0he.macOS.Adobe-Downloader.helper")
super.init()
listener.delegate = self
logger.notice("HelperTool 初始化完成")
2024-11-13 13:20:25 +08:00
}
func run() {
logger.notice("Helper 服务开始运行")
2024-11-13 13:20:25 +08:00
ProcessInfo.processInfo.disableSuddenTermination()
ProcessInfo.processInfo.disableAutomaticTermination("Helper is running")
listener.resume()
logger.notice("XPC Listener 已启动")
2024-11-13 13:20:25 +08:00
RunLoop.current.run()
}
2025-01-14 17:25:08 +08:00
func executeCommand(type: CommandType, path1: String, path2: String, permissions: Int, withReply reply: @escaping (String) -> Void) {
operationQueue.async {
guard let shellCommand = SecureCommandHandler.createCommand(type: type, path1: path1, path2: path2, permissions: permissions) else {
self.logger.error("不安全的路径访问被拒绝")
reply("Error: Invalid path access")
return
}
#if DEBUG
self.logger.notice("收到安全命令执行请求: \(shellCommand, privacy: .public)")
#else
self.logger.notice("收到安全命令执行请求: \(String(shellCommand.prefix(20)))")
#endif
2025-01-14 17:25:08 +08:00
let isSetupCommand = shellCommand.contains("Setup") && shellCommand.contains("--install")
let task = Process()
let outputPipe = Pipe()
let errorPipe = Pipe()
task.standardOutput = outputPipe
task.standardError = errorPipe
task.arguments = ["-c", shellCommand]
task.executableURL = URL(fileURLWithPath: "/bin/sh")
if isSetupCommand {
self.currentTask = task
self.outputPipe = outputPipe
self.outputBuffer = ""
let outputHandle = outputPipe.fileHandleForReading
outputHandle.readabilityHandler = { [weak self] handle in
guard let self = self else { return }
let data = handle.availableData
if let output = String(data: data, encoding: .utf8) {
self.outputBuffer += output
}
}
do {
try task.run()
self.logger.debug("Setup命令开始执行")
reply("Started")
} catch {
let errorMsg = "Error: \(error.localizedDescription)"
self.logger.error("执行失败: \(errorMsg, privacy: .public)")
reply(errorMsg)
}
return
}
do {
try task.run()
self.logger.debug("安全命令开始执行")
} catch {
let errorMsg = "Error: \(error.localizedDescription)"
self.logger.error("执行失败: \(errorMsg, privacy: .public)")
reply(errorMsg)
return
2024-11-13 13:20:25 +08:00
}
2025-01-14 17:25:08 +08:00
let outputHandle = outputPipe.fileHandleForReading
let errorHandle = errorPipe.fileHandleForReading
2025-01-14 17:25:08 +08:00
var output = ""
var errorOutput = ""
2025-01-14 17:25:08 +08:00
outputHandle.readabilityHandler = { handle in
let data = handle.availableData
if let newOutput = String(data: data, encoding: .utf8) {
output += newOutput
}
}
errorHandle.readabilityHandler = { handle in
let data = handle.availableData
if let newError = String(data: data, encoding: .utf8) {
errorOutput += newError
}
}
2025-01-14 17:25:08 +08:00
task.waitUntilExit()
outputHandle.readabilityHandler = nil
errorHandle.readabilityHandler = nil
2025-01-14 17:25:08 +08:00
if task.terminationStatus == 0 {
self.logger.notice("命令执行成功")
let result = output.isEmpty ? "Success" : output.trimmingCharacters(in: .whitespacesAndNewlines)
reply(result)
2025-01-14 17:25:08 +08:00
} else {
let fullErrorMsg = errorOutput.isEmpty ? "Unknown error" : errorOutput.trimmingCharacters(in: .whitespacesAndNewlines)
self.logger.error("命令执行失败,退出码: \(task.terminationStatus), 错误信息: \(fullErrorMsg)")
reply("Error: Command failed with exit code \(task.terminationStatus): \(fullErrorMsg)")
}
}
}
func getInstallationOutput(withReply reply: @escaping (String) -> Void) {
2025-01-14 17:25:08 +08:00
guard let task = currentTask else {
reply("")
return
}
2025-01-14 17:25:08 +08:00
if !task.isRunning {
let exitCode = task.terminationStatus
reply("Exit Code: \(exitCode)")
cleanup()
return
}
2025-01-14 17:25:08 +08:00
if !outputBuffer.isEmpty {
let output = outputBuffer
outputBuffer = ""
reply(output)
} else {
reply("")
}
}
func cleanup() {
if let pipe = outputPipe {
pipe.fileHandleForReading.readabilityHandler = nil
}
currentTask?.terminate()
currentTask = nil
outputPipe = nil
2024-11-13 13:20:25 +08:00
}
}
extension HelperTool: NSXPCListenerDelegate {
func listener(_ listener: NSXPCListener, shouldAcceptNewConnection newConnection: NSXPCConnection) -> Bool {
2025-01-14 17:25:08 +08:00
logger.notice("收到新的XPC连接请求")
let pid = newConnection.processIdentifier
var codeRef: SecCode?
let codeSigningResult = SecCodeCopyGuestWithAttributes(nil,
[kSecGuestAttributePid: pid] as CFDictionary,
[], &codeRef)
guard codeSigningResult == errSecSuccess,
let code = codeRef else {
logger.error("代码签名验证失败: \(pid)")
return false
}
var staticCode: SecStaticCode?
let staticCodeResult = SecCodeCopyStaticCode(code, [], &staticCode)
guard staticCodeResult == errSecSuccess,
let staticCodeRef = staticCode else {
logger.error("获取静态代码失败: \(staticCodeResult)")
return false
}
2025-01-14 17:25:08 +08:00
var requirement: SecRequirement?
let requirementString = "identifier \"com.x1a0he.macOS.Adobe-Downloader\" and anchor apple generic and certificate leaf[subject.CN] = \"Apple Development: x1a0he@outlook.com (LFN2762T4F)\" and certificate 1[field.1.2.840.113635.100.6.2.1] /* exists */"
2025-01-14 17:25:08 +08:00
guard SecRequirementCreateWithString(requirementString as CFString,
[], &requirement) == errSecSuccess,
let req = requirement else {
self.logger.error("签名要求创建失败")
2025-01-14 17:25:08 +08:00
return false
}
let validityResult = SecStaticCodeCheckValidity(staticCodeRef, [], req)
2025-01-14 17:25:08 +08:00
if validityResult != errSecSuccess {
self.logger.error("代码签名验证不匹配: \(validityResult), 要求字符串: \(requirementString)")
var signingInfo: CFDictionary?
if SecCodeCopySigningInformation(staticCodeRef, [], &signingInfo) == errSecSuccess,
let info = signingInfo as? [String: Any] {
self.logger.notice("实际签名信息: \(String(describing: info))")
}
2025-01-14 17:25:08 +08:00
return false
}
let interface = NSXPCInterface(with: HelperToolProtocol.self)
interface.setClasses(NSSet(array: [NSString.self, NSNumber.self]) as! Set<AnyHashable>,
for: #selector(HelperToolProtocol.executeCommand(type:path1:path2:permissions:withReply:)),
argumentIndex: 1,
ofReply: false)
newConnection.exportedInterface = interface
2024-11-13 13:20:25 +08:00
newConnection.exportedObject = self
2024-11-13 13:20:25 +08:00
newConnection.invalidationHandler = { [weak self] in
2025-01-14 17:25:08 +08:00
guard let self = self else { return }
self.logger.notice("XPC连接已断开")
self.connections.remove(newConnection)
if self.connections.isEmpty {
self.cleanup()
}
}
newConnection.interruptionHandler = { [weak self] in
guard let self = self else { return }
self.logger.error("XPC连接中断")
self.connections.remove(newConnection)
if self.connections.isEmpty {
self.cleanup()
}
2024-11-13 13:20:25 +08:00
}
2025-01-14 17:25:08 +08:00
self.connections.insert(newConnection)
2024-11-13 13:20:25 +08:00
newConnection.resume()
2025-01-14 17:25:08 +08:00
logger.notice("新的XPC连接已成功建立当前活动连接数: \(self.connections.count)")
2024-11-13 13:20:25 +08:00
return true
}
}
autoreleasepool {
let helperTool = HelperTool()
helperTool.run()
}