2024-11-13 08:37:50 +08:00
import Foundation
2024-11-15 19:34:29 +08:00
import os . log
2024-11-13 08:37:50 +08:00
2025-01-14 17:25:08 +08:00
@objc enum CommandType : Int {
case install
case uninstall
case moveFile
case setPermissions
case shellCommand
}
2025-08-17 14:04:13 +08:00
@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 :
2025-02-05 23:09:46 +08:00
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 :
2025-02-05 23:09:46 +08:00
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 {
2025-02-05 23:09:46 +08:00
let cleanPath = path . trimmingCharacters ( in : . init ( charactersIn : " \" ' " ) )
2025-01-14 17:25:08 +08:00
let allowedPaths = [ " /Library/Application Support/Adobe " ]
2025-02-05 23:09:46 +08:00
if allowedPaths . contains ( where : { cleanPath . hasPrefix ( $0 ) } ) {
2025-01-14 17:25:08 +08:00
return true
}
let forbiddenPaths = [ " /System " , " /usr " , " /bin " , " /sbin " , " /var " ]
2025-02-05 23:09:46 +08:00
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 > = [ ]
2024-11-13 23:56:07 +08:00
private var currentTask : Process ?
private var outputPipe : Pipe ?
2025-01-14 17:25:08 +08:00
private var outputBuffer : String = " "
2024-11-15 19:34:29 +08:00
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-15 19:34:29 +08:00
2024-11-13 13:20:25 +08:00
override init ( ) {
listener = NSXPCListener ( machServiceName : " com.x1a0he.macOS.Adobe-Downloader.helper " )
super . init ( )
listener . delegate = self
2024-11-15 19:34:29 +08:00
logger . notice ( " HelperTool 初始化完成 " )
2024-11-13 13:20:25 +08:00
}
func run ( ) {
2024-11-15 19:34:29 +08:00
logger . notice ( " Helper 服务开始运行 " )
2024-11-13 13:20:25 +08:00
ProcessInfo . processInfo . disableSuddenTermination ( )
ProcessInfo . processInfo . disableAutomaticTermination ( " Helper is running " )
listener . resume ( )
2024-11-15 19:34:29 +08:00
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
}
2025-02-05 23:09:46 +08:00
#if DEBUG
self . logger . notice ( " 收到安全命令执行请求: \( shellCommand , privacy : . public ) " )
#else
2025-08-17 14:04:13 +08:00
self . logger . notice ( " 收到安全命令执行请求: \( String ( shellCommand . prefix ( 20 ) ) ) " )
2025-02-05 23:09:46 +08:00
#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
}
2024-11-15 19:34:29 +08:00
2025-01-14 17:25:08 +08:00
let outputHandle = outputPipe . fileHandleForReading
2025-08-17 14:04:13 +08:00
let errorHandle = errorPipe . fileHandleForReading
2025-01-14 17:25:08 +08:00
var output = " "
2025-08-17 14:04:13 +08:00
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
}
2024-11-15 19:34:29 +08:00
}
2025-08-17 14:04:13 +08:00
errorHandle . readabilityHandler = { handle in
let data = handle . availableData
if let newError = String ( data : data , encoding : . utf8 ) {
errorOutput += newError
}
}
2024-11-15 19:34:29 +08:00
2025-01-14 17:25:08 +08:00
task . waitUntilExit ( )
2024-11-15 19:34:29 +08:00
outputHandle . readabilityHandler = nil
2025-08-17 14:04:13 +08:00
errorHandle . readabilityHandler = nil
2025-01-14 17:25:08 +08:00
if task . terminationStatus = = 0 {
self . logger . notice ( " 命令执行成功 " )
2025-08-17 14:04:13 +08:00
let result = output . isEmpty ? " Success " : output . trimmingCharacters ( in : . whitespacesAndNewlines )
reply ( result )
2025-01-14 17:25:08 +08:00
} else {
2025-08-17 14:04:13 +08:00
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 ) " )
2024-11-15 19:34:29 +08:00
}
2024-11-13 23:56:07 +08:00
}
}
func getInstallationOutput ( withReply reply : @ escaping ( String ) -> Void ) {
2025-01-14 17:25:08 +08:00
guard let task = currentTask else {
2024-11-13 23:56:07 +08:00
reply ( " " )
return
}
2025-01-14 17:25:08 +08:00
if ! task . isRunning {
let exitCode = task . terminationStatus
reply ( " Exit Code: \( exitCode ) " )
cleanup ( )
2024-11-13 23:56:07 +08:00
return
}
2025-01-14 17:25:08 +08:00
if ! outputBuffer . isEmpty {
let output = outputBuffer
outputBuffer = " "
reply ( output )
2024-11-13 23:56:07 +08:00
} else {
reply ( " " )
}
}
2024-11-15 19:34:29 +08:00
func cleanup ( ) {
2024-11-13 23:56:07 +08:00
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
}
2025-02-05 23:09:46 +08:00
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 ?
2025-02-05 23:09:46 +08:00
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 {
2025-02-05 23:09:46 +08:00
self . logger . error ( " 签名要求创建失败 " )
2025-01-14 17:25:08 +08:00
return false
}
2025-02-05 23:09:46 +08:00
let validityResult = SecStaticCodeCheckValidity ( staticCodeRef , [ ] , req )
2025-01-14 17:25:08 +08:00
if validityResult != errSecSuccess {
2025-02-05 23:09:46 +08:00
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-15 19:34:29 +08:00
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
}
2024-11-15 19:34:29 +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-15 19:34:29 +08:00
2024-11-13 13:20:25 +08:00
return true
}
}
autoreleasepool {
let helperTool = HelperTool ( )
helperTool . run ( )
}