chore: split PrivilegedHelper
This commit is contained in:
parent
7ffc0e3f9e
commit
60691df9c3
@ -43,6 +43,7 @@
|
||||
499A486522EEA3FD00F6C675 /* Array+Safe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 499A486422EEA3FC00F6C675 /* Array+Safe.swift */; };
|
||||
49ABB749236B0F9E00535CD7 /* UnsafePointer+bridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49ABB748236B0F9E00535CD7 /* UnsafePointer+bridge.swift */; };
|
||||
49B1086A216A356D0064FFCE /* String+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49B10869216A356D0064FFCE /* String+Extension.swift */; };
|
||||
49B4575D244F4A2A00463C39 /* PrivilegedHelperManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49B4575C244F4A2A00463C39 /* PrivilegedHelperManager.swift */; };
|
||||
49BC061C212931F4005A0FE7 /* AboutViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49BC061B212931F4005A0FE7 /* AboutViewController.swift */; };
|
||||
49C9EF64223E78F5005D8B6A /* ClashProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49C9EF63223E78F5005D8B6A /* ClashProxy.swift */; };
|
||||
49CF3B2120CD7463001EBF94 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49CF3B2020CD7463001EBF94 /* AppDelegate.swift */; };
|
||||
@ -151,6 +152,7 @@
|
||||
499A486422EEA3FC00F6C675 /* Array+Safe.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Array+Safe.swift"; sourceTree = "<group>"; };
|
||||
49ABB748236B0F9E00535CD7 /* UnsafePointer+bridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UnsafePointer+bridge.swift"; sourceTree = "<group>"; };
|
||||
49B10869216A356D0064FFCE /* String+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extension.swift"; sourceTree = "<group>"; };
|
||||
49B4575C244F4A2A00463C39 /* PrivilegedHelperManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivilegedHelperManager.swift; sourceTree = "<group>"; };
|
||||
49BC061B212931F4005A0FE7 /* AboutViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutViewController.swift; sourceTree = "<group>"; };
|
||||
49C9EF63223E78F5005D8B6A /* ClashProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClashProxy.swift; sourceTree = "<group>"; };
|
||||
49CF3B1D20CD7463001EBF94 /* ClashX.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ClashX.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
@ -264,6 +266,7 @@
|
||||
F977FAAB2366790500C17F1F /* AutoUpgardeManager.swift */,
|
||||
F977FAAD23669D6400C17F1F /* ConnectionManager.swift */,
|
||||
F9E754CF239CC21F00CEE7CC /* WebPortalManager.swift */,
|
||||
49B4575C244F4A2A00463C39 /* PrivilegedHelperManager.swift */,
|
||||
);
|
||||
path = Managers;
|
||||
sourceTree = "<group>";
|
||||
@ -676,6 +679,7 @@
|
||||
F977FAAC2366790500C17F1F /* AutoUpgardeManager.swift in Sources */,
|
||||
499A485822ED715200F6C675 /* RemoteConfigModel.swift in Sources */,
|
||||
49D176AB23575BB20093DD7B /* ProxyGroupMenuItemView.swift in Sources */,
|
||||
49B4575D244F4A2A00463C39 /* PrivilegedHelperManager.swift in Sources */,
|
||||
4966E9E32118153A00A391FB /* NSUserNotificationCenter+Extension.swift in Sources */,
|
||||
F9E754D2239CC28D00CEE7CC /* NSAlert+Extension.swift in Sources */,
|
||||
499976C821359F0400E7BF83 /* ClashWebViewContoller.swift in Sources */,
|
||||
|
@ -85,7 +85,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
|
||||
// install proxy helper
|
||||
_ = ClashResourceManager.check()
|
||||
SystemProxyManager.shared.checkInstall()
|
||||
PrivilegedHelperManager.shared.checkInstall()
|
||||
ConfigFileManager.copySampleConfigIfNeed()
|
||||
|
||||
PFMoveToApplicationsFolderIfNecessary()
|
||||
|
@ -39,7 +39,7 @@ class ConfigFileManager {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@discardableResult
|
||||
static func backupAndRemoveConfigFile() -> Bool {
|
||||
let path = kDefaultConfigFilePath
|
||||
@ -50,7 +50,6 @@ class ConfigFileManager {
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
static func copySampleConfigIfNeed() {
|
||||
if !FileManager.default.fileExists(atPath: kDefaultConfigFilePath) {
|
||||
let path = Bundle.main.path(forResource: "sampleConfig", ofType: "yaml")!
|
||||
|
277
ClashX/General/Managers/PrivilegedHelperManager.swift
Normal file
277
ClashX/General/Managers/PrivilegedHelperManager.swift
Normal file
@ -0,0 +1,277 @@
|
||||
//
|
||||
// PrivilegedHelperManager.swift
|
||||
// ClashX
|
||||
//
|
||||
// Created by yicheng on 2020/4/21.
|
||||
// Copyright © 2020 west2online. All rights reserved.
|
||||
//
|
||||
|
||||
import AppKit
|
||||
import ServiceManagement
|
||||
|
||||
class PrivilegedHelperManager {
|
||||
private var cancelInstallCheck = false
|
||||
private var authRef: AuthorizationRef?
|
||||
private var connection: NSXPCConnection?
|
||||
private var _helper: ProxyConfigRemoteProcessProtocol?
|
||||
static let machServiceName = "com.west2online.ClashX.ProxyConfigHelper"
|
||||
|
||||
static let shared = PrivilegedHelperManager()
|
||||
init() {
|
||||
// super.init()
|
||||
initAuthorizationRef()
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
func checkInstall() {
|
||||
Logger.log("checkInstall", level: .debug)
|
||||
while !cancelInstallCheck && !helperStatus() {
|
||||
Logger.log("need to install helper", level: .debug)
|
||||
if Thread.isMainThread {
|
||||
notifyInstall()
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
self.notifyInstall()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func initAuthorizationRef() {
|
||||
// Create an empty AuthorizationRef
|
||||
let status = AuthorizationCreate(nil, nil, AuthorizationFlags(), &authRef)
|
||||
if status != OSStatus(errAuthorizationSuccess) {
|
||||
Logger.log("initAuthorizationRef AuthorizationCreate failed", level: .error)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
/// Install new helper daemon
|
||||
private func installHelperDaemon() -> DaemonInstallResult {
|
||||
Logger.log("installHelperDaemon", level: .info)
|
||||
|
||||
defer {
|
||||
connection?.invalidate()
|
||||
connection = nil
|
||||
_helper = nil
|
||||
}
|
||||
|
||||
// Create authorization reference for the user
|
||||
var authRef: AuthorizationRef?
|
||||
var authStatus = AuthorizationCreate(nil, nil, [], &authRef)
|
||||
|
||||
// Check if the reference is valid
|
||||
guard authStatus == errAuthorizationSuccess else {
|
||||
Logger.log("Authorization failed: \(authStatus)", level: .error)
|
||||
return .authorizationFail
|
||||
}
|
||||
|
||||
// Ask user for the admin privileges to install the
|
||||
var authItem = AuthorizationItem(name: (kSMRightBlessPrivilegedHelper as NSString).utf8String!, valueLength: 0, value: nil, flags: 0)
|
||||
var authRights = withUnsafeMutablePointer(to: &authItem) { pointer in
|
||||
AuthorizationRights(count: 1, items: pointer)
|
||||
}
|
||||
let flags: AuthorizationFlags = [[], .interactionAllowed, .extendRights, .preAuthorize]
|
||||
authStatus = AuthorizationCreate(&authRights, nil, flags, &authRef)
|
||||
defer {
|
||||
if let ref = authRef {
|
||||
AuthorizationFree(ref, [])
|
||||
}
|
||||
}
|
||||
// Check if the authorization went succesfully
|
||||
guard authStatus == errAuthorizationSuccess else {
|
||||
Logger.log("Couldn't obtain admin privileges: \(authStatus)", level: .error)
|
||||
return .getAdmainFail
|
||||
}
|
||||
|
||||
// Launch the privileged helper using SMJobBless tool
|
||||
var error: Unmanaged<CFError>?
|
||||
|
||||
if SMJobBless(kSMDomainSystemLaunchd, PrivilegedHelperManager.machServiceName as CFString, authRef, &error) == false {
|
||||
let blessError = error!.takeRetainedValue() as Error
|
||||
Logger.log("Bless Error: \(blessError)", level: .error)
|
||||
return .blessError((blessError as NSError).code)
|
||||
}
|
||||
|
||||
Logger.log("\(PrivilegedHelperManager.machServiceName) installed successfully", level: .info)
|
||||
return .success
|
||||
}
|
||||
|
||||
func authData() -> Data? {
|
||||
guard let authRef = authRef else { return nil }
|
||||
var authRefExtForm = AuthorizationExternalForm()
|
||||
|
||||
// Make an external form of the AuthorizationRef
|
||||
var status = AuthorizationMakeExternalForm(authRef, &authRefExtForm)
|
||||
if status != OSStatus(errAuthorizationSuccess) {
|
||||
Logger.log("AppviewController: AuthorizationMakeExternalForm failed", level: .error)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Add all or update required authorization right definition to the authorization database
|
||||
var currentRight: CFDictionary?
|
||||
|
||||
// Try to get the authorization right definition from the database
|
||||
status = AuthorizationRightGet(AppAuthorizationRights.rightName.utf8String!, ¤tRight)
|
||||
|
||||
if status == errAuthorizationDenied {
|
||||
let defaultRules = AppAuthorizationRights.rightDefaultRule
|
||||
status = AuthorizationRightSet(authRef,
|
||||
AppAuthorizationRights.rightName.utf8String!,
|
||||
defaultRules as CFDictionary,
|
||||
AppAuthorizationRights.rightDescription,
|
||||
nil, "Common" as CFString)
|
||||
}
|
||||
|
||||
// We need to put the AuthorizationRef to a form that can be passed through inter process call
|
||||
let authData = NSData(bytes: &authRefExtForm, length: kAuthorizationExternalFormLength)
|
||||
return authData as Data
|
||||
}
|
||||
|
||||
private func helperConnection() -> NSXPCConnection? {
|
||||
// Check that the connection is valid before trying to do an inter process call to helper
|
||||
if connection == nil {
|
||||
connection = NSXPCConnection(machServiceName: PrivilegedHelperManager.machServiceName, options: NSXPCConnection.Options.privileged)
|
||||
connection?.remoteObjectInterface = NSXPCInterface(with: ProxyConfigRemoteProcessProtocol.self)
|
||||
connection?.invalidationHandler = {
|
||||
[weak self] in
|
||||
guard let self = self else { return }
|
||||
self.connection?.invalidationHandler = nil
|
||||
OperationQueue.main.addOperation {
|
||||
self.connection = nil
|
||||
self._helper = nil
|
||||
Logger.log("XPC Connection Invalidated")
|
||||
}
|
||||
}
|
||||
connection?.resume()
|
||||
}
|
||||
return connection
|
||||
}
|
||||
|
||||
func helper(failture: (() -> Void)? = nil) -> ProxyConfigRemoteProcessProtocol? {
|
||||
if _helper == nil {
|
||||
guard let newHelper = helperConnection()?.remoteObjectProxyWithErrorHandler({ error in
|
||||
Logger.log("Helper connection was closed with error: \(error)")
|
||||
failture?()
|
||||
}) as? ProxyConfigRemoteProcessProtocol else { return nil }
|
||||
_helper = newHelper
|
||||
}
|
||||
return _helper
|
||||
}
|
||||
|
||||
private func helperStatus() -> Bool {
|
||||
let helperURL = Bundle.main.bundleURL.appendingPathComponent("Contents/Library/LaunchServices/" + PrivilegedHelperManager.machServiceName)
|
||||
guard
|
||||
let helperBundleInfo = CFBundleCopyInfoDictionaryForURL(helperURL as CFURL) as? [String: Any],
|
||||
let helperVersion = helperBundleInfo["CFBundleShortVersionString"] as? String,
|
||||
let helper = self.helper() else {
|
||||
return false
|
||||
}
|
||||
let helperFileExists = FileManager.default.fileExists(atPath: "/Library/PrivilegedHelperTools/\(PrivilegedHelperManager.machServiceName)")
|
||||
let timeout: TimeInterval = helperFileExists ? 15 : 2
|
||||
var installed = false
|
||||
let time = Date()
|
||||
let semaphore = DispatchSemaphore(value: 0)
|
||||
helper.getVersion { installedHelperVersion in
|
||||
Logger.log("helper version \(installedHelperVersion ?? "") require version \(helperVersion)", level: .debug)
|
||||
installed = installedHelperVersion == helperVersion
|
||||
semaphore.signal()
|
||||
}
|
||||
_ = semaphore.wait(timeout: DispatchTime.now() + timeout)
|
||||
let interval = Date().timeIntervalSince(time)
|
||||
Logger.log("check helper using time: \(interval)")
|
||||
return installed
|
||||
}
|
||||
}
|
||||
|
||||
extension PrivilegedHelperManager {
|
||||
private func notifyInstall() {
|
||||
guard showInstallHelperAlert() else { exit(0) }
|
||||
|
||||
if cancelInstallCheck {
|
||||
return
|
||||
}
|
||||
|
||||
let result = installHelperDaemon()
|
||||
if case .success = result {
|
||||
return
|
||||
}
|
||||
result.alertAction()
|
||||
NSAlert.alert(with: result.alertContent)
|
||||
}
|
||||
|
||||
private func showInstallHelperAlert() -> Bool {
|
||||
let alert = NSAlert()
|
||||
alert.messageText = NSLocalizedString("ClashX needs to install/update a helper tool with administrator privileges to set system proxy quickly.If not helper tool installed, ClashX won't be able to set your system proxy", comment: "")
|
||||
alert.alertStyle = .warning
|
||||
alert.addButton(withTitle: NSLocalizedString("Install", comment: ""))
|
||||
alert.addButton(withTitle: NSLocalizedString("Quit", comment: ""))
|
||||
alert.addButton(withTitle: NSLocalizedString("Cancel", comment: ""))
|
||||
switch alert.runModal() {
|
||||
case .alertFirstButtonReturn:
|
||||
return true
|
||||
case .alertThirdButtonReturn:
|
||||
cancelInstallCheck = true
|
||||
Logger.log("cancelInstallCheck = true", level: .error)
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate struct AppAuthorizationRights {
|
||||
static let rightName: NSString = "\(PrivilegedHelperManager.machServiceName).config" as NSString
|
||||
static let rightDefaultRule: Dictionary = adminRightsRule
|
||||
static let rightDescription: CFString = "ProxyConfigHelper wants to configure your proxy setting'" as CFString
|
||||
static var adminRightsRule: [String: Any] = ["class": "user",
|
||||
"group": "admin",
|
||||
"timeout": 0,
|
||||
"version": 1]
|
||||
}
|
||||
|
||||
fileprivate enum DaemonInstallResult {
|
||||
case success
|
||||
case authorizationFail
|
||||
case getAdmainFail
|
||||
case blessError(Int)
|
||||
|
||||
var alertContent: String {
|
||||
switch self {
|
||||
case .success:
|
||||
return ""
|
||||
case .authorizationFail: return "Create authorization fail!"
|
||||
case .getAdmainFail: return "Get admain authorization fail!"
|
||||
case let .blessError(code):
|
||||
switch code {
|
||||
case kSMErrorInternalFailure: return "blessError: kSMErrorInternalFailure"
|
||||
case kSMErrorInvalidSignature: return "blessError: kSMErrorInvalidSignature"
|
||||
case kSMErrorAuthorizationFailure: return "blessError: kSMErrorAuthorizationFailure"
|
||||
case kSMErrorToolNotValid: return "blessError: kSMErrorToolNotValid"
|
||||
case kSMErrorJobNotFound: return "blessError: kSMErrorJobNotFound"
|
||||
case kSMErrorServiceUnavailable: return "blessError: kSMErrorServiceUnavailable"
|
||||
case kSMErrorJobNotFound: return "blessError: kSMErrorJobNotFound"
|
||||
case kSMErrorJobMustBeEnabled: return "ClashX Helper is disabled by other process. Please run \"sudo launchctl enable system/\(PrivilegedHelperManager.machServiceName)\" in your terminal. The command has been copied to your pasteboard"
|
||||
case kSMErrorInvalidPlist: return "blessError: kSMErrorInvalidPlist"
|
||||
default:
|
||||
return "bless unknown error:\(code)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func alertAction() {
|
||||
switch self {
|
||||
case let .blessError(code):
|
||||
switch code {
|
||||
case kSMErrorJobMustBeEnabled:
|
||||
NSPasteboard.general.clearContents()
|
||||
NSPasteboard.general.setString("sudo launchctl enable system/\(PrivilegedHelperManager.machServiceName)", forType: .string)
|
||||
default:
|
||||
break
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
@ -12,10 +12,6 @@ import ServiceManagement
|
||||
class SystemProxyManager: NSObject {
|
||||
static let shared = SystemProxyManager()
|
||||
|
||||
private static let machServiceName = "com.west2online.ClashX.ProxyConfigHelper"
|
||||
private var authRef: AuthorizationRef?
|
||||
private var connection: NSXPCConnection?
|
||||
private var _helper: ProxyConfigRemoteProcessProtocol?
|
||||
private var savedProxyInfo: [String: Any] {
|
||||
get {
|
||||
return UserDefaults.standard.dictionary(forKey: "kSavedProxyInfo") ?? [:]
|
||||
@ -34,35 +30,16 @@ class SystemProxyManager: NSObject {
|
||||
}
|
||||
}
|
||||
|
||||
private var cancelInstallCheck = false
|
||||
|
||||
// MARK: - LifeCycle
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
initAuthorizationRef()
|
||||
private var helper: ProxyConfigRemoteProcessProtocol? {
|
||||
PrivilegedHelperManager.shared.helper()
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
func checkInstall() {
|
||||
Logger.log("checkInstall", level: .debug)
|
||||
while !cancelInstallCheck && !helperStatus() {
|
||||
Logger.log("need to install helper", level: .debug)
|
||||
if Thread.isMainThread {
|
||||
notifyInstall()
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
self.notifyInstall()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
private let authData = PrivilegedHelperManager.shared.authData
|
||||
|
||||
func saveProxy() {
|
||||
guard !disableRestoreProxy else { return }
|
||||
Logger.log("saveProxy", level: .debug)
|
||||
helper()?.getCurrentProxySetting({ [weak self] info in
|
||||
helper?.getCurrentProxySetting({ [weak self] info in
|
||||
Logger.log("saveProxy done", level: .debug)
|
||||
if let info = info as? [String: Any] {
|
||||
self?.savedProxyInfo = info
|
||||
@ -82,7 +59,7 @@ class SystemProxyManager: NSObject {
|
||||
return
|
||||
}
|
||||
Logger.log("enableProxy", level: .debug)
|
||||
helper()?.enableProxy(withPort: Int32(port), socksPort: Int32(socksPort), authData: authData(), error: { error in
|
||||
helper?.enableProxy(withPort: Int32(port), socksPort: Int32(socksPort), authData: authData(), error: { error in
|
||||
if let error = error {
|
||||
Logger.log("enableProxy \(error)", level: .error)
|
||||
}
|
||||
@ -99,7 +76,7 @@ class SystemProxyManager: NSObject {
|
||||
Logger.log("disableProxy", level: .debug)
|
||||
|
||||
if disableRestoreProxy || forceDisable {
|
||||
helper()?.disableProxy(withAuthData: authData(), error: { error in
|
||||
helper?.disableProxy(withAuthData: authData(), error: { error in
|
||||
if let error = error {
|
||||
Logger.log("disableProxy \(error)", level: .error)
|
||||
}
|
||||
@ -107,7 +84,7 @@ class SystemProxyManager: NSObject {
|
||||
return
|
||||
}
|
||||
|
||||
helper()?.restoreProxy(withCurrentPort: Int32(port), socksPort: Int32(socksPort), info: savedProxyInfo, authData: authData(), error: { error in
|
||||
helper?.restoreProxy(withCurrentPort: Int32(port), socksPort: Int32(socksPort), info: savedProxyInfo, authData: authData(), error: { error in
|
||||
if let error = error {
|
||||
Logger.log("restoreProxy \(error)", level: .error)
|
||||
}
|
||||
@ -131,241 +108,4 @@ class SystemProxyManager: NSObject {
|
||||
disableRestoreProxy = !disableRestoreProxy
|
||||
updateMenuItemStatus(sender)
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func initAuthorizationRef() {
|
||||
// Create an empty AuthorizationRef
|
||||
let status = AuthorizationCreate(nil, nil, AuthorizationFlags(), &authRef)
|
||||
if status != OSStatus(errAuthorizationSuccess) {
|
||||
Logger.log("initAuthorizationRef AuthorizationCreate failed", level: .error)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
/// Install new helper daemon
|
||||
private func installHelperDaemon() -> DaemonInstallResult {
|
||||
Logger.log("installHelperDaemon", level: .info)
|
||||
|
||||
defer {
|
||||
connection?.invalidate()
|
||||
connection = nil
|
||||
_helper = nil
|
||||
}
|
||||
|
||||
// Create authorization reference for the user
|
||||
var authRef: AuthorizationRef?
|
||||
var authStatus = AuthorizationCreate(nil, nil, [], &authRef)
|
||||
|
||||
// Check if the reference is valid
|
||||
guard authStatus == errAuthorizationSuccess else {
|
||||
Logger.log("Authorization failed: \(authStatus)", level: .error)
|
||||
return .authorizationFail
|
||||
}
|
||||
|
||||
// Ask user for the admin privileges to install the
|
||||
var authItem = AuthorizationItem(name: kSMRightBlessPrivilegedHelper, valueLength: 0, value: nil, flags: 0)
|
||||
var authRights = AuthorizationRights(count: 1, items: &authItem)
|
||||
let flags: AuthorizationFlags = [[], .interactionAllowed, .extendRights, .preAuthorize]
|
||||
authStatus = AuthorizationCreate(&authRights, nil, flags, &authRef)
|
||||
defer {
|
||||
if let ref = authRef {
|
||||
AuthorizationFree(ref, [])
|
||||
}
|
||||
}
|
||||
// Check if the authorization went succesfully
|
||||
guard authStatus == errAuthorizationSuccess else {
|
||||
Logger.log("Couldn't obtain admin privileges: \(authStatus)", level: .error)
|
||||
return .getAdmainFail
|
||||
}
|
||||
|
||||
// Launch the privileged helper using SMJobBless tool
|
||||
var error: Unmanaged<CFError>?
|
||||
|
||||
if SMJobBless(kSMDomainSystemLaunchd, SystemProxyManager.machServiceName as CFString, authRef, &error) == false {
|
||||
let blessError = error!.takeRetainedValue() as Error
|
||||
Logger.log("Bless Error: \(blessError)", level: .error)
|
||||
return .blessError((blessError as NSError).code)
|
||||
}
|
||||
|
||||
Logger.log("\(SystemProxyManager.machServiceName) installed successfully", level: .info)
|
||||
return .success
|
||||
}
|
||||
|
||||
private func authData() -> Data? {
|
||||
guard let authRef = authRef else { return nil }
|
||||
var authRefExtForm = AuthorizationExternalForm()
|
||||
|
||||
// Make an external form of the AuthorizationRef
|
||||
var status = AuthorizationMakeExternalForm(authRef, &authRefExtForm)
|
||||
if status != OSStatus(errAuthorizationSuccess) {
|
||||
Logger.log("AppviewController: AuthorizationMakeExternalForm failed", level: .error)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Add all or update required authorization right definition to the authorization database
|
||||
var currentRight: CFDictionary?
|
||||
|
||||
// Try to get the authorization right definition from the database
|
||||
status = AuthorizationRightGet(AppAuthorizationRights.rightName.utf8String!, ¤tRight)
|
||||
|
||||
if status == errAuthorizationDenied {
|
||||
let defaultRules = AppAuthorizationRights.rightDefaultRule
|
||||
status = AuthorizationRightSet(authRef,
|
||||
AppAuthorizationRights.rightName.utf8String!,
|
||||
defaultRules as CFDictionary,
|
||||
AppAuthorizationRights.rightDescription,
|
||||
nil, "Common" as CFString)
|
||||
}
|
||||
|
||||
// We need to put the AuthorizationRef to a form that can be passed through inter process call
|
||||
let authData = NSData(bytes: &authRefExtForm, length: kAuthorizationExternalFormLength)
|
||||
return authData as Data
|
||||
}
|
||||
|
||||
private func helperConnection() -> NSXPCConnection? {
|
||||
// Check that the connection is valid before trying to do an inter process call to helper
|
||||
if connection == nil {
|
||||
connection = NSXPCConnection(machServiceName: SystemProxyManager.machServiceName, options: NSXPCConnection.Options.privileged)
|
||||
connection?.remoteObjectInterface = NSXPCInterface(with: ProxyConfigRemoteProcessProtocol.self)
|
||||
connection?.invalidationHandler = {
|
||||
[weak self] in
|
||||
guard let self = self else { return }
|
||||
self.connection?.invalidationHandler = nil
|
||||
OperationQueue.main.addOperation {
|
||||
self.connection = nil
|
||||
self._helper = nil
|
||||
Logger.log("XPC Connection Invalidated")
|
||||
}
|
||||
}
|
||||
connection?.resume()
|
||||
}
|
||||
return connection
|
||||
}
|
||||
|
||||
private func helper(failture: (() -> Void)? = nil) -> ProxyConfigRemoteProcessProtocol? {
|
||||
if _helper == nil {
|
||||
guard let newHelper = helperConnection()?.remoteObjectProxyWithErrorHandler({ error in
|
||||
Logger.log("Helper connection was closed with error: \(error)")
|
||||
failture?()
|
||||
}) as? ProxyConfigRemoteProcessProtocol else { return nil }
|
||||
_helper = newHelper
|
||||
}
|
||||
return _helper
|
||||
}
|
||||
|
||||
private func helperStatus() -> Bool {
|
||||
let helperURL = Bundle.main.bundleURL.appendingPathComponent("Contents/Library/LaunchServices/" + SystemProxyManager.machServiceName)
|
||||
guard
|
||||
let helperBundleInfo = CFBundleCopyInfoDictionaryForURL(helperURL as CFURL) as? [String: Any],
|
||||
let helperVersion = helperBundleInfo["CFBundleShortVersionString"] as? String,
|
||||
let helper = self.helper() else {
|
||||
return false
|
||||
}
|
||||
let helperFileExists = FileManager.default.fileExists(atPath: "/Library/PrivilegedHelperTools/com.west2online.ClashX.ProxyConfigHelper")
|
||||
let timeout: TimeInterval = helperFileExists ? 15 : 2
|
||||
var installed = false
|
||||
let time = Date()
|
||||
let semaphore = DispatchSemaphore(value: 0)
|
||||
helper.getVersion { installedHelperVersion in
|
||||
Logger.log("helper version \(installedHelperVersion ?? "") require version \(helperVersion)", level: .debug)
|
||||
installed = installedHelperVersion == helperVersion
|
||||
semaphore.signal()
|
||||
}
|
||||
_ = semaphore.wait(timeout: DispatchTime.now() + timeout)
|
||||
let interval = Date().timeIntervalSince(time)
|
||||
Logger.log("check helper using time: \(interval)")
|
||||
return installed
|
||||
}
|
||||
}
|
||||
|
||||
extension SystemProxyManager {
|
||||
private func notifyInstall() {
|
||||
guard showInstallHelperAlert() else { exit(0) }
|
||||
|
||||
if cancelInstallCheck {
|
||||
return
|
||||
}
|
||||
|
||||
let result = installHelperDaemon()
|
||||
if case .success = result {
|
||||
return
|
||||
}
|
||||
result.alertAction()
|
||||
NSAlert.alert(with: result.alertContent)
|
||||
}
|
||||
|
||||
private func showInstallHelperAlert() -> Bool {
|
||||
let alert = NSAlert()
|
||||
alert.messageText = NSLocalizedString("ClashX needs to install/update a helper tool with administrator privileges to set system proxy quickly.If not helper tool installed, ClashX won't be able to set your system proxy", comment: "")
|
||||
alert.alertStyle = .warning
|
||||
alert.addButton(withTitle: NSLocalizedString("Install", comment: ""))
|
||||
alert.addButton(withTitle: NSLocalizedString("Quit", comment: ""))
|
||||
alert.addButton(withTitle: NSLocalizedString("Cancel", comment: ""))
|
||||
switch alert.runModal() {
|
||||
case .alertFirstButtonReturn:
|
||||
return true
|
||||
case .alertThirdButtonReturn:
|
||||
cancelInstallCheck = true
|
||||
Logger.log("cancelInstallCheck = true", level: .error)
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate struct AppAuthorizationRights {
|
||||
static let rightName: NSString = "com.west2online.ClashX.ProxyConfigHelper.config"
|
||||
static let rightDefaultRule: Dictionary = adminRightsRule
|
||||
static let rightDescription: CFString = "ProxyConfigHelper wants to configure your proxy setting'" as CFString
|
||||
static var adminRightsRule: [String: Any] = ["class": "user",
|
||||
"group": "admin",
|
||||
"timeout": 0,
|
||||
"version": 1]
|
||||
}
|
||||
|
||||
fileprivate enum DaemonInstallResult {
|
||||
case success
|
||||
case authorizationFail
|
||||
case getAdmainFail
|
||||
case blessError(Int)
|
||||
|
||||
var alertContent: String {
|
||||
switch self {
|
||||
case .success:
|
||||
return ""
|
||||
case .authorizationFail: return "Create authorization fail!"
|
||||
case .getAdmainFail: return "Get admain authorization fail!"
|
||||
case let .blessError(code):
|
||||
switch code {
|
||||
case kSMErrorInternalFailure: return "blessError: kSMErrorInternalFailure"
|
||||
case kSMErrorInvalidSignature: return "blessError: kSMErrorInvalidSignature"
|
||||
case kSMErrorAuthorizationFailure: return "blessError: kSMErrorAuthorizationFailure"
|
||||
case kSMErrorToolNotValid: return "blessError: kSMErrorToolNotValid"
|
||||
case kSMErrorJobNotFound: return "blessError: kSMErrorJobNotFound"
|
||||
case kSMErrorServiceUnavailable: return "blessError: kSMErrorServiceUnavailable"
|
||||
case kSMErrorJobNotFound: return "blessError: kSMErrorJobNotFound"
|
||||
case kSMErrorJobMustBeEnabled: return "ClashX Helper is disabled by other process. Please run \"sudo launchctl enable system/com.west2online.ClashX.ProxyConfigHelper\" in your terminal. The command has been copied to your pasteboard"
|
||||
case kSMErrorInvalidPlist: return "blessError: kSMErrorInvalidPlist"
|
||||
default:
|
||||
return "bless unknown error:\(code)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func alertAction() {
|
||||
switch self {
|
||||
case let .blessError(code):
|
||||
switch code {
|
||||
case kSMErrorJobMustBeEnabled:
|
||||
NSPasteboard.general.clearContents()
|
||||
NSPasteboard.general.setString("sudo launchctl enable system/com.west2online.ClashX.ProxyConfigHelper", forType: .string)
|
||||
default:
|
||||
break
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user