Files
ShadowsocksX-NG/ShadowsocksX-NG2/SystemProxyHelper/SystemProxyWriter.swift

182 lines
6.9 KiB
Swift

import Foundation
import SystemConfiguration
/// root 进程内的 SystemConfiguration 写入器:GUI 域决策的机械
/// 执行者。apply 覆盖活动网络位置的每个服务,只改 typed 字段并保留未建模
/// 键,值已等价时跳过提交;clear 无条件移除全部位置全部服务的完整 Proxies
/// 字典。root 身份直接创建 SCPreferences,不走授权对话框。
enum SystemProxyWriter {
static func perform(
_ request: SystemProxyHelperEngine.Request
) throws -> SystemProxyHelperEngine.Outcome {
switch request {
case .apply(let configuration):
return .applied(try apply(configuration))
case .clear:
try clear()
return .cleared
}
}
static func apply(_ configuration: SystemProxyConfiguration) throws -> SystemProxyWriteOutcome {
try withPreferences { preferences in
try lockPreferencesOrThrow(preferences)
defer { SCPreferencesUnlock(preferences) }
let services = try activeServiceSnapshots(in: preferences)
guard !services.isEmpty else { throw SystemProxyError.noProxyServices }
let plan = try SystemProxyPlanner.makeApplyPlan(
services: services.map(\.plannerState), configuration: configuration)
guard !plan.writes.isEmpty else { return .unchanged }
let servicesByID = Dictionary(uniqueKeysWithValues: services.map { ($0.identifier, $0) })
for write in plan.writes {
guard let service = servicesByID[write.identifier] else {
throw SystemProxyError.cannotWriteService(write.identifier.serviceID)
}
if service.proxyProtocol == nil {
_ = SCNetworkServiceAddProtocolType(
service.networkService, kSCNetworkProtocolTypeProxies)
}
guard
let proxyProtocol = service.proxyProtocol
?? SCNetworkServiceCopyProtocol(service.networkService, kSCNetworkProtocolTypeProxies),
setConfiguration(write.configuration, on: proxyProtocol)
else { throw SystemProxyError.cannotWriteService(write.identifier.serviceID) }
}
try commitAndApply(preferences)
return .written
}
}
static func clear() throws {
try withPreferences { preferences in
try lockPreferencesOrThrow(preferences)
defer { SCPreferencesUnlock(preferences) }
let services = try allServiceSnapshots(in: preferences)
let plan = SystemProxyPlanner.makeClearPlan(services: services.map(\.plannerState))
guard !plan.writes.isEmpty else { return }
let servicesByID = Dictionary(uniqueKeysWithValues: services.map { ($0.identifier, $0) })
for write in plan.writes {
guard let service = servicesByID[write.identifier],
let proxyProtocol = service.proxyProtocol,
setConfiguration(nil, on: proxyProtocol)
else { throw SystemProxyError.cannotWriteService(write.identifier.serviceID) }
}
try commitAndApply(preferences)
}
}
// MARK: - SystemConfiguration 读取与写入
private struct ServiceSnapshot {
let identifier: SystemProxyServiceIdentifier
let networkService: SCNetworkService
let proxyProtocol: SCNetworkProtocol?
let configuration: Data?
var plannerState: SystemProxyServiceState {
SystemProxyServiceState(identifier: identifier, configuration: configuration)
}
}
private static func withPreferences<T>(_ body: (SCPreferences) throws -> T) throws -> T {
guard
let preferences = SCPreferencesCreate(
nil, SystemProxyHelperIdentity.machServiceName as CFString, nil)
else { throw SystemProxyError.preferencesUnavailable }
return try body(preferences)
}
private static func activeServiceSnapshots(
in preferences: SCPreferences
) throws -> [ServiceSnapshot] {
guard let set = SCNetworkSetCopyCurrent(preferences) else {
throw SystemProxyError.noCurrentNetworkSet
}
guard let locationID = SCNetworkSetGetSetID(set) as String? else {
throw SystemProxyError.noCurrentNetworkSet
}
return try serviceSnapshots(in: set, locationID: locationID)
}
private static func allServiceSnapshots(
in preferences: SCPreferences
) throws -> [ServiceSnapshot] {
guard let sets = SCNetworkSetCopyAll(preferences) as? [SCNetworkSet], !sets.isEmpty else {
throw SystemProxyError.noNetworkLocations
}
return try sets.flatMap { set -> [ServiceSnapshot] in
guard let locationID = SCNetworkSetGetSetID(set) as String? else {
return [ServiceSnapshot]()
}
return try serviceSnapshots(in: set, locationID: locationID)
}
}
private static func serviceSnapshots(
in set: SCNetworkSet, locationID: String
) throws -> [ServiceSnapshot] {
guard let services = SCNetworkSetCopyServices(set) as? [SCNetworkService] else {
return []
}
return try services.compactMap { service in
guard let serviceID = SCNetworkServiceGetServiceID(service) as String? else { return nil }
let proxyProtocol = SCNetworkServiceCopyProtocol(service, kSCNetworkProtocolTypeProxies)
let configurationValue: CFPropertyList?
if let proxyProtocol {
configurationValue = SCNetworkProtocolGetConfiguration(proxyProtocol)
} else {
configurationValue = nil
}
return ServiceSnapshot(
identifier: SystemProxyServiceIdentifier(
locationID: locationID, serviceID: serviceID),
networkService: service,
proxyProtocol: proxyProtocol,
configuration: try propertyListData(from: configurationValue, serviceID: serviceID))
}
}
private static func propertyListData(
from value: CFPropertyList?, serviceID: String
) throws -> Data? {
guard let value else { return nil }
guard let dictionary = value as? [String: Any] else {
throw SystemProxyError.unreadableService(serviceID)
}
return try SystemProxyPlanner.propertyListData(from: dictionary, serviceID: serviceID)
}
private static func setConfiguration(_ data: Data?, on proxyProtocol: SCNetworkProtocol) -> Bool {
guard let data else { return SCNetworkProtocolSetConfiguration(proxyProtocol, nil) }
guard
let propertyList = try? PropertyListSerialization.propertyList(
from: data, options: [], format: nil),
let dictionary = propertyList as? [String: Any]
else { return false }
return SCNetworkProtocolSetConfiguration(proxyProtocol, dictionary as CFDictionary)
}
private static func commitAndApply(_ preferences: SCPreferences) throws {
guard SCPreferencesCommitChanges(preferences) else {
throw SystemProxyError.commitFailed(systemConfigurationError())
}
guard SCPreferencesApplyChanges(preferences) else {
throw SystemProxyError.applyFailed(systemConfigurationError())
}
}
private static func systemConfigurationError() -> String {
String(cString: SCErrorString(SCError()))
}
}
private func lockPreferencesOrThrow(_ preferences: SCPreferences) throws {
guard SCPreferencesLock(preferences, true) else {
throw SystemProxyError.preferencesBusy
}
}