ClashX.Meta/ClashX/General/Managers/SystemProxyManager.swift

267 lines
10 KiB
Swift
Raw Normal View History

//
// SystemProxyManager.swift
// ClashX
//
// Created by yichengchen on 2019/8/17.
// Copyright © 2019 west2online. All rights reserved.
//
import AppKit
2019-10-20 13:40:50 +08:00
import ServiceManagement
class SystemProxyManager: NSObject {
static let shared = SystemProxyManager()
2019-10-20 13:40:50 +08:00
private static let machServiceName = "com.west2online.ClashX.ProxyConfigHelper"
private var authRef: AuthorizationRef?
private var connection: NSXPCConnection?
private var _helper: ProxyConfigRemoteProcessProtocol?
2019-10-20 13:40:50 +08:00
private var savedProxyInfo: [String: Any] {
get {
return UserDefaults.standard.dictionary(forKey: "kSavedProxyInfo") ?? [:]
}
set {
UserDefaults.standard.set(newValue, forKey: "kSavedProxyInfo")
}
}
2019-10-20 13:40:50 +08:00
private var disableRestoreProxy: Bool {
get {
return UserDefaults.standard.bool(forKey: "kDisableRestoreProxy")
}
set {
UserDefaults.standard.set(newValue, forKey: "kDisableRestoreProxy")
}
}
2019-10-20 13:40:50 +08:00
// MARK: - LifeCycle
2019-10-20 13:40:50 +08:00
override init() {
super.init()
initAuthorizationRef()
}
2019-10-20 13:40:50 +08:00
// MARK: - Public
2019-10-20 13:40:50 +08:00
func checkInstall() {
Logger.log("checkInstall", level: .debug)
2019-09-14 17:26:43 +08:00
let installed = helperStatus()
2019-10-20 13:40:50 +08:00
if installed { return }
Logger.log("need to install helper", level: .debug)
2019-09-14 17:26:43 +08:00
if Thread.isMainThread {
2019-10-20 13:40:50 +08:00
notifyInstall()
2019-09-14 17:26:43 +08:00
} else {
DispatchQueue.main.async {
self.notifyInstall()
}
}
}
2019-10-20 13:40:50 +08:00
func saveProxy() {
2019-10-20 13:40:50 +08:00
guard !disableRestoreProxy else { return }
Logger.log("saveProxy", level: .debug)
helper()?.getCurrentProxySetting({ [weak self] info in
Logger.log("saveProxy done", level: .debug)
2019-10-20 13:40:50 +08:00
if let info = info as? [String: Any] {
self?.savedProxyInfo = info
}
})
}
2019-10-20 13:40:50 +08:00
func enableProxy(port: Int, socksPort: Int) {
Logger.log("enableProxy", level: .debug)
helper()?.enableProxy(withPort: Int32(port), socksPort: Int32(socksPort), authData: authData(), error: { error in
2019-10-20 13:40:50 +08:00
if let error = error {
Logger.log("enableProxy \(error)", level: .error)
}
})
}
2019-10-20 13:40:50 +08:00
func disableProxy(port: Int, socksPort: Int) {
Logger.log("disableProxy", level: .debug)
2019-10-20 13:40:50 +08:00
if disableRestoreProxy {
2019-10-20 13:40:50 +08:00
helper()?.disableProxy(withAuthData: authData(), error: { error in
if let error = error {
Logger.log("disableProxy \(error)", level: .error)
}
})
return
}
2019-10-20 13:40:50 +08:00
helper()?.restoreProxy(withCurrentPort: Int32(port), socksPort: Int32(socksPort), info: savedProxyInfo, authData: authData(), error: { error in
2019-10-20 13:40:50 +08:00
if let error = error {
Logger.log("restoreProxy \(error)", level: .error)
}
2019-10-20 13:40:50 +08:00
})
}
2019-10-20 13:40:50 +08:00
// MARK: - Private
2019-10-20 13:40:50 +08:00
private func initAuthorizationRef() {
// Create an empty AuthorizationRef
let status = AuthorizationCreate(nil, nil, AuthorizationFlags(), &authRef)
2019-10-20 13:40:50 +08:00
if status != OSStatus(errAuthorizationSuccess) {
Logger.log("initAuthorizationRef AuthorizationCreate failed", level: .error)
return
}
}
2019-10-20 13:40:50 +08:00
/// Install new helper daemon
private func installHelperDaemon() {
Logger.log("installHelperDaemon", level: .info)
2019-08-21 12:03:04 +08:00
// Create authorization reference for the user
var authRef: AuthorizationRef?
var authStatus = AuthorizationCreate(nil, nil, [], &authRef)
2019-10-20 13:40:50 +08:00
// Check if the reference is valid
guard authStatus == errAuthorizationSuccess else {
Logger.log("Authorization failed: \(authStatus)", level: .error)
return
}
2019-10-20 13:40:50 +08:00
// 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
}
2019-10-20 13:40:50 +08:00
// Launch the privileged helper using SMJobBless tool
2019-10-20 13:40:50 +08:00
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)
} else {
Logger.log("\(SystemProxyManager.machServiceName) installed successfully", level: .info)
}
2019-10-20 13:40:50 +08:00
connection?.invalidate()
connection = nil
_helper = nil
}
2019-10-20 13:40:50 +08:00
private func authData() -> Data? {
2019-10-20 13:40:50 +08:00
guard let authRef = authRef else { return nil }
var authRefExtForm = AuthorizationExternalForm()
2019-10-20 13:40:50 +08:00
// Make an external form of the AuthorizationRef
var status = AuthorizationMakeExternalForm(authRef, &authRefExtForm)
2019-10-20 13:40:50 +08:00
if status != OSStatus(errAuthorizationSuccess) {
Logger.log("AppviewController: AuthorizationMakeExternalForm failed", level: .error)
return nil
}
2019-10-20 13:40:50 +08:00
// Add all or update required authorization right definition to the authorization database
2019-10-20 13:40:50 +08:00
var currentRight: CFDictionary?
// Try to get the authorization right definition from the database
status = AuthorizationRightGet(AppAuthorizationRights.rightName.utf8String!, &currentRight)
2019-10-20 13:40:50 +08:00
if status == errAuthorizationDenied {
let defaultRules = AppAuthorizationRights.rightDefaultRule
status = AuthorizationRightSet(authRef,
AppAuthorizationRights.rightName.utf8String!,
defaultRules as CFDictionary,
AppAuthorizationRights.rightDescription,
nil, "Common" as CFString)
}
2019-10-20 13:40:50 +08:00
// We need to put the AuthorizationRef to a form that can be passed through inter process call
2019-10-20 13:40:50 +08:00
let authData = NSData(bytes: &authRefExtForm, length: kAuthorizationExternalFormLength)
return authData as Data
}
2019-10-20 13:40:50 +08:00
private func helperConnection() -> NSXPCConnection? {
// Check that the connection is valid before trying to do an inter process call to helper
2019-10-20 13:40:50 +08:00
if connection == nil {
connection = NSXPCConnection(machServiceName: SystemProxyManager.machServiceName, options: NSXPCConnection.Options.privileged)
connection?.remoteObjectInterface = NSXPCInterface(with: ProxyConfigRemoteProcessProtocol.self)
connection?.invalidationHandler = {
2019-08-21 12:03:04 +08:00
[weak self] in
2019-10-20 13:40:50 +08:00
guard let self = self else { return }
self.connection?.invalidationHandler = nil
2019-10-20 13:40:50 +08:00
OperationQueue.main.addOperation {
self.connection = nil
2019-08-21 12:03:04 +08:00
self._helper = nil
Logger.log("XPC Connection Invalidated")
}
}
connection?.resume()
}
return connection
}
2019-10-20 13:40:50 +08:00
private func helper(failture: (() -> Void)? = nil) -> ProxyConfigRemoteProcessProtocol? {
if _helper == nil {
guard let newHelper = self.helperConnection()?.remoteObjectProxyWithErrorHandler({ error in
Logger.log("Helper connection was closed with error: \(error)")
failture?()
}) as? ProxyConfigRemoteProcessProtocol else { return nil }
_helper = newHelper
}
return _helper
}
2019-10-20 13:40:50 +08:00
2019-09-14 17:26:43 +08:00
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 {
2019-10-20 13:40:50 +08:00
return false
}
let helperFileExists = FileManager.default.fileExists(atPath: "/Library/PrivilegedHelperTools/com.west2online.ClashX.ProxyConfigHelper")
let timeout: TimeInterval = helperFileExists ? 8 : 2
var installed = false
2019-10-11 19:27:21 +08:00
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
2019-09-14 17:26:43 +08:00
semaphore.signal()
}
2019-10-20 13:40:50 +08:00
_ = semaphore.wait(timeout: DispatchTime.now() + timeout)
2019-10-11 19:27:21 +08:00
let interval = Date().timeIntervalSince(time)
Logger.log("check helper using time: \(interval)")
2019-09-14 17:26:43 +08:00
return installed
}
}
extension SystemProxyManager {
private func notifyInstall() {
2019-10-20 13:40:50 +08:00
guard showInstallHelperAlert() else { exit(0) }
2019-09-14 17:26:43 +08:00
installHelperDaemon()
}
2019-10-20 13:40:50 +08:00
private func showInstallHelperAlert() -> Bool {
let alert = NSAlert()
alert.messageText = NSLocalizedString("ClashX needs to install a helper tool with administrator privileges to set system proxy quickly.", comment: "")
alert.alertStyle = .warning
alert.addButton(withTitle: NSLocalizedString("Install", comment: ""))
alert.addButton(withTitle: NSLocalizedString("Quit", comment: ""))
return alert.runModal() == .alertFirstButtonReturn
}
}
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
2019-10-20 13:40:50 +08:00
static var adminRightsRule: [String: Any] = ["class": "user",
"group": "admin",
"timeout": 0,
"version": 1]
}