refactor: Fix macOS standalone application
This commit is contained in:
@@ -10,6 +10,7 @@ public extension Date {
|
||||
var relativeFormat: String {
|
||||
let formatter = RelativeDateTimeFormatter()
|
||||
formatter.unitsStyle = .full
|
||||
formatter.dateTimeStyle = .named
|
||||
return formatter.localizedString(for: self, relativeTo: Date())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,61 +119,62 @@ public extension UTType {
|
||||
}
|
||||
|
||||
#if !os(tvOS)
|
||||
// MARK: - FileDocument for Export
|
||||
|
||||
public struct ProfileExportDocument: FileDocument {
|
||||
public static var readableContentTypes: [UTType] { [.profile] }
|
||||
// MARK: - FileDocument for Export
|
||||
|
||||
public let data: Data
|
||||
public let filename: String
|
||||
public struct ProfileExportDocument: FileDocument {
|
||||
public static var readableContentTypes: [UTType] { [.profile] }
|
||||
|
||||
public init(content: LibboxProfileContent) throws {
|
||||
guard let encoded = content.encode() else {
|
||||
throw NSError(domain: "ProfileExportDocument", code: -1, userInfo: [NSLocalizedDescriptionKey: "Failed to encode profile"])
|
||||
public let data: Data
|
||||
public let filename: String
|
||||
|
||||
public init(content: LibboxProfileContent) throws {
|
||||
guard let encoded = content.encode() else {
|
||||
throw NSError(domain: "ProfileExportDocument", code: -1, userInfo: [NSLocalizedDescriptionKey: "Failed to encode profile"])
|
||||
}
|
||||
data = encoded
|
||||
filename = "\(content.name).bpf"
|
||||
}
|
||||
data = encoded
|
||||
filename = "\(content.name).bpf"
|
||||
}
|
||||
|
||||
public init(configuration: ReadConfiguration) throws {
|
||||
guard let data = configuration.file.regularFileContents else {
|
||||
throw CocoaError(.fileReadCorruptFile)
|
||||
public init(configuration: ReadConfiguration) throws {
|
||||
guard let data = configuration.file.regularFileContents else {
|
||||
throw CocoaError(.fileReadCorruptFile)
|
||||
}
|
||||
self.data = data
|
||||
filename = "profile.bpf"
|
||||
}
|
||||
self.data = data
|
||||
filename = "profile.bpf"
|
||||
}
|
||||
|
||||
public func fileWrapper(configuration _: WriteConfiguration) throws -> FileWrapper {
|
||||
FileWrapper(regularFileWithContents: data)
|
||||
}
|
||||
}
|
||||
|
||||
public struct ProfileJSONExportDocument: FileDocument {
|
||||
public static var readableContentTypes: [UTType] { [.json] }
|
||||
|
||||
public let content: String
|
||||
public let filename: String
|
||||
|
||||
public init(jsonContent: String, name: String) {
|
||||
content = jsonContent
|
||||
filename = "\(name).json"
|
||||
}
|
||||
|
||||
public init(configuration: ReadConfiguration) throws {
|
||||
guard let data = configuration.file.regularFileContents,
|
||||
let content = String(data: data, encoding: .utf8)
|
||||
else {
|
||||
throw CocoaError(.fileReadCorruptFile)
|
||||
public func fileWrapper(configuration _: WriteConfiguration) throws -> FileWrapper {
|
||||
FileWrapper(regularFileWithContents: data)
|
||||
}
|
||||
self.content = content
|
||||
filename = "profile.json"
|
||||
}
|
||||
|
||||
public func fileWrapper(configuration _: WriteConfiguration) throws -> FileWrapper {
|
||||
guard let data = content.data(using: .utf8) else {
|
||||
throw CocoaError(.fileWriteInapplicableStringEncoding)
|
||||
public struct ProfileJSONExportDocument: FileDocument {
|
||||
public static var readableContentTypes: [UTType] { [.json] }
|
||||
|
||||
public let content: String
|
||||
public let filename: String
|
||||
|
||||
public init(jsonContent: String, name: String) {
|
||||
content = jsonContent
|
||||
filename = "\(name).json"
|
||||
}
|
||||
|
||||
public init(configuration: ReadConfiguration) throws {
|
||||
guard let data = configuration.file.regularFileContents,
|
||||
let content = String(data: data, encoding: .utf8)
|
||||
else {
|
||||
throw CocoaError(.fileReadCorruptFile)
|
||||
}
|
||||
self.content = content
|
||||
filename = "profile.json"
|
||||
}
|
||||
|
||||
public func fileWrapper(configuration _: WriteConfiguration) throws -> FileWrapper {
|
||||
guard let data = content.data(using: .utf8) else {
|
||||
throw CocoaError(.fileWriteInapplicableStringEncoding)
|
||||
}
|
||||
return FileWrapper(regularFileWithContents: data)
|
||||
}
|
||||
return FileWrapper(regularFileWithContents: data)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -29,7 +29,7 @@ public extension Profile {
|
||||
if await SharedPreferences.selectedProfileID.get() == id {
|
||||
if let profile = try? await ExtensionProfile.load() {
|
||||
if await profile.status == .connected {
|
||||
try LibboxNewStandaloneCommandClient()!.serviceReload()
|
||||
try await profile.reloadService()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import BinaryCodable
|
||||
import Foundation
|
||||
import GRDB
|
||||
import os
|
||||
|
||||
private let logger = Logger(category: "SharedPreferences")
|
||||
|
||||
extension SharedPreferences {
|
||||
public class Preference<T: Codable> {
|
||||
@@ -16,7 +19,7 @@ extension SharedPreferences {
|
||||
do {
|
||||
return try await SharedPreferences.read(name) ?? defaultValue
|
||||
} catch {
|
||||
NSLog("read preferences error: \(error)")
|
||||
logger.error("read preferences error: \(error)")
|
||||
return defaultValue
|
||||
}
|
||||
}
|
||||
@@ -31,7 +34,7 @@ extension SharedPreferences {
|
||||
do {
|
||||
try await SharedPreferences.write(name, newValue)
|
||||
} catch {
|
||||
NSLog("write preferences error: \(error)")
|
||||
logger.error("write preferences error: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import Foundation
|
||||
import Libbox
|
||||
import os
|
||||
|
||||
private let logger = Logger(category: "CommandClient")
|
||||
|
||||
public struct LogEntry: Identifiable {
|
||||
public let id = UUID()
|
||||
@@ -124,6 +127,13 @@ public class CommandClient: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
public func clearLogs() {
|
||||
logBatchTimer?.cancel()
|
||||
logBatchTimer = nil
|
||||
pendingLogs.removeAll()
|
||||
logList.removeAll()
|
||||
}
|
||||
|
||||
public func filterConnectionsNow() {
|
||||
guard let message = rawConnections else {
|
||||
return
|
||||
@@ -222,7 +232,7 @@ public class CommandClient: ObservableObject {
|
||||
commandClient.isConnected = false
|
||||
}
|
||||
if let message {
|
||||
NSLog("client disconnected: \(message)")
|
||||
logger.debug("client disconnected: \(message)")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,7 +244,7 @@ public class CommandClient: ObservableObject {
|
||||
|
||||
func clearLogs() {
|
||||
DispatchQueue.main.async { [self] in
|
||||
commandClient.logList.removeAll()
|
||||
commandClient.clearLogs()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
#if os(macOS)
|
||||
import Foundation
|
||||
import Libbox
|
||||
import os
|
||||
|
||||
private let logger = Logger(category: "CommandXPC")
|
||||
|
||||
@objc public protocol CommandXPCProtocol {
|
||||
func connectToCommandServer(reply: @escaping (FileHandle?, NSError?) -> Void)
|
||||
func registerUserServiceEndpoint(_ endpoint: NSXPCListenerEndpoint?, reply: @escaping (NSError?) -> Void)
|
||||
func extensionRequirements(reply: @escaping (Bool, Bool, NSError?) -> Void)
|
||||
}
|
||||
|
||||
class CommandXPCService: NSObject, NSXPCListenerDelegate {
|
||||
let socketPath: String
|
||||
var commandServer: LibboxCommandServer?
|
||||
|
||||
private let serviceReadyLock = NSLock()
|
||||
private var _serviceReady = false
|
||||
private var serviceReadyContinuations: [CheckedContinuation<Void, Never>] = []
|
||||
|
||||
init(socketPath: String) {
|
||||
self.socketPath = socketPath
|
||||
}
|
||||
|
||||
func waitForServiceReady() async {
|
||||
serviceReadyLock.lock()
|
||||
if _serviceReady {
|
||||
serviceReadyLock.unlock()
|
||||
return
|
||||
}
|
||||
await withCheckedContinuation { continuation in
|
||||
serviceReadyContinuations.append(continuation)
|
||||
serviceReadyLock.unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func markServiceReady() {
|
||||
serviceReadyLock.lock()
|
||||
_serviceReady = true
|
||||
let continuations = serviceReadyContinuations
|
||||
serviceReadyContinuations.removeAll()
|
||||
serviceReadyLock.unlock()
|
||||
for continuation in continuations {
|
||||
continuation.resume()
|
||||
}
|
||||
}
|
||||
|
||||
func markServiceNotReady() {
|
||||
serviceReadyLock.lock()
|
||||
_serviceReady = false
|
||||
serviceReadyLock.unlock()
|
||||
}
|
||||
|
||||
func listener(_: NSXPCListener, shouldAcceptNewConnection newConnection: NSXPCConnection) -> Bool {
|
||||
let allowedBundleIDs = [AppConfiguration.packageName + ".standalone"]
|
||||
guard XPCConnectionValidator.validateConnection(
|
||||
newConnection,
|
||||
teamID: AppConfiguration.teamID,
|
||||
allowedBundleIDs: allowedBundleIDs
|
||||
) else {
|
||||
let info = XPCConnectionValidator.getConnectionInfo(newConnection)
|
||||
logger.warning("Rejected XPC connection: pid=\(info.pid), bundleID=\(info.bundleID ?? "unknown"), teamID=\(info.teamID ?? "unknown")")
|
||||
return false
|
||||
}
|
||||
|
||||
let exportedInterface = NSXPCInterface(with: CommandXPCProtocol.self)
|
||||
CommandXPC.configureInterface(exportedInterface)
|
||||
newConnection.exportedInterface = exportedInterface
|
||||
newConnection.exportedObject = CommandXPCHandler(service: self)
|
||||
newConnection.resume()
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
private class CommandXPCHandler: NSObject, CommandXPCProtocol {
|
||||
private let service: CommandXPCService
|
||||
|
||||
init(service: CommandXPCService) {
|
||||
self.service = service
|
||||
}
|
||||
|
||||
func connectToCommandServer(reply: @escaping (FileHandle?, NSError?) -> Void) {
|
||||
do {
|
||||
let handle = try connectToUnixSocket(path: service.socketPath)
|
||||
reply(handle, nil)
|
||||
} catch {
|
||||
reply(nil, error as NSError)
|
||||
}
|
||||
}
|
||||
|
||||
func registerUserServiceEndpoint(_ endpoint: NSXPCListenerEndpoint?, reply: @escaping (NSError?) -> Void) {
|
||||
if let endpoint {
|
||||
UserServiceEndpointRegistry.shared.update(endpoint)
|
||||
} else {
|
||||
UserServiceEndpointRegistry.shared.clear()
|
||||
}
|
||||
reply(nil)
|
||||
}
|
||||
|
||||
func extensionRequirements(reply: @escaping (Bool, Bool, NSError?) -> Void) {
|
||||
Task {
|
||||
await service.waitForServiceReady()
|
||||
guard let commandServer = service.commandServer else {
|
||||
reply(false, false, NSError(domain: "CommandXPC", code: -1, userInfo: [
|
||||
NSLocalizedDescriptionKey: "Command server not available",
|
||||
]))
|
||||
return
|
||||
}
|
||||
let needWIFI = commandServer.needWIFIState()
|
||||
let needProcess = commandServer.needFindProcess()
|
||||
reply(needWIFI, needProcess, nil)
|
||||
}
|
||||
}
|
||||
|
||||
private func connectToUnixSocket(path: String) throws -> FileHandle {
|
||||
let fd = socket(AF_UNIX, SOCK_STREAM, 0)
|
||||
guard fd >= 0 else {
|
||||
throw NSError(domain: "CommandXPC", code: Int(errno), userInfo: [
|
||||
NSLocalizedDescriptionKey: "Failed to create socket: \(String(cString: strerror(errno)))",
|
||||
])
|
||||
}
|
||||
|
||||
var addr = sockaddr_un()
|
||||
addr.sun_family = sa_family_t(AF_UNIX)
|
||||
let pathSize = MemoryLayout.size(ofValue: addr.sun_path)
|
||||
withUnsafeMutableBytes(of: &addr.sun_path) { buffer in
|
||||
_ = path.withCString { cString in
|
||||
strncpy(buffer.baseAddress!.assumingMemoryBound(to: CChar.self), cString, pathSize - 1)
|
||||
}
|
||||
}
|
||||
|
||||
let connectResult = withUnsafePointer(to: &addr) { ptr in
|
||||
ptr.withMemoryRebound(to: sockaddr.self, capacity: 1) { sockaddrPtr in
|
||||
connect(fd, sockaddrPtr, socklen_t(MemoryLayout<sockaddr_un>.size))
|
||||
}
|
||||
}
|
||||
|
||||
guard connectResult >= 0 else {
|
||||
close(fd)
|
||||
throw NSError(domain: "CommandXPC", code: Int(errno), userInfo: [
|
||||
NSLocalizedDescriptionKey: "Failed to connect to \(path): \(String(cString: strerror(errno)))",
|
||||
])
|
||||
}
|
||||
|
||||
return FileHandle(fileDescriptor: fd, closeOnDealloc: false)
|
||||
}
|
||||
}
|
||||
|
||||
public class CommandXPCDialer: NSObject, LibboxXPCDialerProtocol {
|
||||
public static let shared = CommandXPCDialer()
|
||||
|
||||
public func dialXPC(_ ret0_: UnsafeMutablePointer<Int32>?) throws {
|
||||
let semaphore = DispatchSemaphore(value: 0)
|
||||
var result: Int32 = -1
|
||||
var resultError: Error?
|
||||
|
||||
let machServiceName = AppConfiguration.appGroupID + ".system"
|
||||
let connection = NSXPCConnection(machServiceName: machServiceName)
|
||||
let remoteInterface = NSXPCInterface(with: CommandXPCProtocol.self)
|
||||
CommandXPC.configureInterface(remoteInterface)
|
||||
connection.remoteObjectInterface = remoteInterface
|
||||
connection.resume()
|
||||
|
||||
let proxy = connection.remoteObjectProxyWithErrorHandler { error in
|
||||
logger.error("XPC proxy error: \(error.localizedDescription)")
|
||||
resultError = error
|
||||
semaphore.signal()
|
||||
} as! CommandXPCProtocol
|
||||
|
||||
proxy.connectToCommandServer { handle, error in
|
||||
if let error {
|
||||
logger.error("connectToCommandServer error: \(error.localizedDescription)")
|
||||
resultError = error
|
||||
} else if let handle {
|
||||
result = dup(handle.fileDescriptor)
|
||||
}
|
||||
semaphore.signal()
|
||||
}
|
||||
|
||||
semaphore.wait()
|
||||
connection.invalidate()
|
||||
|
||||
if let error = resultError {
|
||||
throw error
|
||||
}
|
||||
if result < 0 {
|
||||
logger.error("dialXPC failed: No file handle returned")
|
||||
throw NSError(domain: "CommandXPCDialer", code: -1, userInfo: [
|
||||
NSLocalizedDescriptionKey: "No file handle returned",
|
||||
])
|
||||
}
|
||||
ret0_?.pointee = result
|
||||
}
|
||||
}
|
||||
|
||||
public enum CommandXPC {
|
||||
public static func configureInterface(_ interface: NSXPCInterface) {
|
||||
let fileHandleClasses = NSSet(array: [FileHandle.self]) as! Set<AnyHashable>
|
||||
interface.setClasses(
|
||||
fileHandleClasses,
|
||||
for: #selector(CommandXPCProtocol.connectToCommandServer(reply:)),
|
||||
argumentIndex: 0,
|
||||
ofReply: true
|
||||
)
|
||||
let endpointClasses = NSSet(array: [NSXPCListenerEndpoint.self]) as! Set<AnyHashable>
|
||||
interface.setClasses(
|
||||
endpointClasses,
|
||||
for: #selector(CommandXPCProtocol.registerUserServiceEndpoint(_:reply:)),
|
||||
argumentIndex: 0,
|
||||
ofReply: false
|
||||
)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -1,12 +1,141 @@
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
public struct AlertState: Equatable {
|
||||
public var title: String
|
||||
public var message: String
|
||||
public var primaryButton: ButtonState?
|
||||
public var secondaryButton: ButtonState?
|
||||
public var onDismiss: (() -> Void)?
|
||||
|
||||
public struct ButtonState: Equatable {
|
||||
public var label: String
|
||||
public var role: ButtonRole?
|
||||
public var action: (() -> Void)?
|
||||
|
||||
public init(label: String, role: ButtonRole? = nil, action: (() -> Void)? = nil) {
|
||||
self.label = label
|
||||
self.role = role
|
||||
self.action = action
|
||||
}
|
||||
|
||||
public static func == (lhs: ButtonState, rhs: ButtonState) -> Bool {
|
||||
lhs.label == rhs.label && lhs.role == rhs.role
|
||||
}
|
||||
|
||||
public static func `default`(_ label: String, action: (() -> Void)? = nil) -> ButtonState {
|
||||
ButtonState(label: label, action: action)
|
||||
}
|
||||
|
||||
public static func cancel(_ label: String = String(localized: "Cancel"), action: (() -> Void)? = nil) -> ButtonState {
|
||||
ButtonState(label: label, role: .cancel, action: action)
|
||||
}
|
||||
|
||||
public static func destructive(_ label: String, action: (() -> Void)? = nil) -> ButtonState {
|
||||
ButtonState(label: label, role: .destructive, action: action)
|
||||
}
|
||||
}
|
||||
|
||||
public init(error: Error, dismiss: (() -> Void)? = nil) {
|
||||
self.init(errorMessage: error.localizedDescription, dismiss: dismiss)
|
||||
}
|
||||
|
||||
public init(errorMessage: String, dismiss: (() -> Void)? = nil) {
|
||||
title = String(localized: "Error")
|
||||
message = errorMessage
|
||||
primaryButton = .default(String(localized: "Ok"), action: dismiss)
|
||||
secondaryButton = nil
|
||||
onDismiss = nil
|
||||
}
|
||||
|
||||
public init(title: String, message: String, dismissButton: ButtonState? = nil) {
|
||||
self.title = title
|
||||
self.message = message
|
||||
primaryButton = dismissButton ?? .default(String(localized: "Ok"))
|
||||
secondaryButton = nil
|
||||
onDismiss = nil
|
||||
}
|
||||
|
||||
public init(title: String, message: String, primaryButton: ButtonState, secondaryButton: ButtonState) {
|
||||
self.title = title
|
||||
self.message = message
|
||||
self.primaryButton = primaryButton
|
||||
self.secondaryButton = secondaryButton
|
||||
onDismiss = nil
|
||||
}
|
||||
|
||||
public init(title: String, message: String, primaryButton: ButtonState, secondaryButton: ButtonState, onDismiss: @escaping () -> Void) {
|
||||
self.title = title
|
||||
self.message = message
|
||||
self.primaryButton = primaryButton
|
||||
self.secondaryButton = secondaryButton
|
||||
self.onDismiss = onDismiss
|
||||
}
|
||||
|
||||
public static func == (lhs: AlertState, rhs: AlertState) -> Bool {
|
||||
lhs.title == rhs.title && lhs.message == rhs.message &&
|
||||
lhs.primaryButton == rhs.primaryButton && lhs.secondaryButton == rhs.secondaryButton
|
||||
}
|
||||
}
|
||||
|
||||
public extension View {
|
||||
@ViewBuilder
|
||||
func alert(_ binding: Binding<AlertState?>) -> some View {
|
||||
alert(
|
||||
binding.wrappedValue?.title ?? "",
|
||||
isPresented: Binding(
|
||||
get: { binding.wrappedValue != nil },
|
||||
set: { newValue, _ in
|
||||
if !newValue {
|
||||
binding.wrappedValue?.onDismiss?()
|
||||
binding.wrappedValue = nil
|
||||
}
|
||||
}
|
||||
),
|
||||
presenting: binding.wrappedValue
|
||||
) { alertState in
|
||||
if let secondary = alertState.secondaryButton {
|
||||
Button(role: alertState.primaryButton?.role) {
|
||||
alertState.primaryButton?.action?()
|
||||
} label: {
|
||||
Text(alertState.primaryButton?.label ?? "Ok")
|
||||
}
|
||||
Button(role: secondary.role) {
|
||||
secondary.action?()
|
||||
} label: {
|
||||
Text(secondary.label)
|
||||
}
|
||||
} else if let primary = alertState.primaryButton {
|
||||
Button(role: primary.role) {
|
||||
primary.action?()
|
||||
} label: {
|
||||
Text(primary.label)
|
||||
}
|
||||
}
|
||||
} message: { alertState in
|
||||
Text(alertState.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public struct ImportRemoteProfileRequest: Hashable, Identifiable {
|
||||
public var id: String { url }
|
||||
public let name: String
|
||||
public let url: String
|
||||
|
||||
public init(name: String, url: String) {
|
||||
self.name = name
|
||||
self.url = url
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
public class ExtensionEnvironments: ObservableObject {
|
||||
@Published public var commandClient = CommandClient([.log, .status, .groups, .clashMode, .connections])
|
||||
@Published public var extensionProfileLoading = true
|
||||
@Published public var extensionProfile: ExtensionProfile?
|
||||
@Published public var emptyProfiles = false
|
||||
@Published public var pendingImportRemoteProfile: ImportRemoteProfileRequest?
|
||||
|
||||
public let profileUpdate = ObjectWillChangePublisher()
|
||||
public let selectedProfileUpdate = ObjectWillChangePublisher()
|
||||
@@ -14,12 +143,6 @@ public class ExtensionEnvironments: ObservableObject {
|
||||
|
||||
public init() {}
|
||||
|
||||
nonisolated deinit {
|
||||
Task { @MainActor in
|
||||
commandClient.disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
public func postReload() {
|
||||
Task {
|
||||
await reload()
|
||||
|
||||
@@ -22,14 +22,17 @@ public class ExtensionPlatformInterface: NSObject, LibboxPlatformInterfaceProtoc
|
||||
|
||||
private func openTun0(_ options: LibboxTunOptionsProtocol?, _ ret0_: UnsafeMutablePointer<Int32>?) async throws {
|
||||
guard let options else {
|
||||
throw NSError(domain: "nil options", code: 0)
|
||||
throw NSError(domain: "ExtensionPlatformInterface", code: 0, userInfo: [NSLocalizedDescriptionKey: String(localized: "Nil options")])
|
||||
}
|
||||
guard let ret0_ else {
|
||||
throw NSError(domain: "nil return pointer", code: 0)
|
||||
throw NSError(domain: "ExtensionPlatformInterface", code: 0, userInfo: [NSLocalizedDescriptionKey: String(localized: "Nil return pointer")])
|
||||
}
|
||||
|
||||
let autoRouteUseSubRangesByDefault = await SharedPreferences.autoRouteUseSubRangesByDefault.get()
|
||||
let excludeAPNs = await SharedPreferences.excludeAPNsRoute.get()
|
||||
let prefs = tunnel.overridePreferences ?? ExtensionProvider.OverridePreferences()
|
||||
let autoRouteUseSubRangesByDefault = prefs.autoRouteUseSubRangesByDefault
|
||||
let excludeAPNs = prefs.excludeAPNsRoute
|
||||
let excludeDefaultRoute = prefs.excludeDefaultRoute
|
||||
let systemProxyEnabled = prefs.systemProxyEnabled
|
||||
|
||||
let settings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: "127.0.0.1")
|
||||
if options.getAutoRoute() {
|
||||
@@ -78,7 +81,7 @@ public class ExtensionPlatformInterface: NSObject, LibboxPlatformInterfaceProtoc
|
||||
let ipv4RoutePrefix = inet4RouteExcludeAddressIterator.next()!
|
||||
ipv4ExcludeRoutes.append(NEIPv4Route(destinationAddress: ipv4RoutePrefix.address(), subnetMask: ipv4RoutePrefix.mask()))
|
||||
}
|
||||
if await SharedPreferences.excludeDefaultRoute.get(), !ipv4Routes.isEmpty {
|
||||
if excludeDefaultRoute, !ipv4Routes.isEmpty {
|
||||
if !ipv4ExcludeRoutes.contains(where: { it in
|
||||
it.destinationAddress == "0.0.0.0" && it.destinationSubnetMask == "255.255.255.254"
|
||||
}) {
|
||||
@@ -134,7 +137,7 @@ public class ExtensionPlatformInterface: NSObject, LibboxPlatformInterfaceProtoc
|
||||
ipv6ExcludeRoutes.append(NEIPv6Route(destinationAddress: ipv6RoutePrefix.address(), networkPrefixLength: NSNumber(value: ipv6RoutePrefix.prefix())))
|
||||
}
|
||||
|
||||
if await SharedPreferences.excludeDefaultRoute.get(), !ipv6Routes.isEmpty {
|
||||
if excludeDefaultRoute, !ipv6Routes.isEmpty {
|
||||
if !ipv6ExcludeRoutes.contains(where: { it in
|
||||
it.destinationAddress == "::" && it.destinationNetworkPrefixLength == 127
|
||||
}) {
|
||||
@@ -152,7 +155,7 @@ public class ExtensionPlatformInterface: NSObject, LibboxPlatformInterfaceProtoc
|
||||
let proxyServer = NEProxyServer(address: options.getHTTPProxyServer(), port: Int(options.getHTTPProxyServerPort()))
|
||||
proxySettings.httpServer = proxyServer
|
||||
proxySettings.httpsServer = proxyServer
|
||||
if await SharedPreferences.systemProxyEnabled.get() {
|
||||
if systemProxyEnabled {
|
||||
proxySettings.httpEnabled = true
|
||||
proxySettings.httpsEnabled = true
|
||||
}
|
||||
@@ -194,7 +197,7 @@ public class ExtensionPlatformInterface: NSObject, LibboxPlatformInterfaceProtoc
|
||||
if tunFdFromLoop != -1 {
|
||||
ret0_.pointee = tunFdFromLoop
|
||||
} else {
|
||||
throw NSError(domain: "missing file descriptor", code: 0)
|
||||
throw NSError(domain: "ExtensionPlatformInterface", code: 0, userInfo: [NSLocalizedDescriptionKey: String(localized: "Missing file descriptor")])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,16 +207,29 @@ public class ExtensionPlatformInterface: NSObject, LibboxPlatformInterfaceProtoc
|
||||
|
||||
public func autoDetectControl(_: Int32) throws {}
|
||||
|
||||
public func findConnectionOwner(_: Int32, sourceAddress _: String?, sourcePort _: Int32, destinationAddress _: String?, destinationPort _: Int32, ret0_ _: UnsafeMutablePointer<Int32>?) throws {
|
||||
throw NSError(domain: "not implemented", code: 0)
|
||||
}
|
||||
|
||||
public func packageName(byUid _: Int32, error _: NSErrorPointer) -> String {
|
||||
""
|
||||
}
|
||||
|
||||
public func uid(byPackageName _: String?, ret0_ _: UnsafeMutablePointer<Int32>?) throws {
|
||||
throw NSError(domain: "not implemented", code: 0)
|
||||
public func findConnectionOwner(_ ipProtocol: Int32, sourceAddress: String?, sourcePort: Int32, destinationAddress: String?, destinationPort: Int32) throws -> LibboxConnectionOwner {
|
||||
#if os(macOS)
|
||||
if Variant.useSystemExtension {
|
||||
guard let sourceAddress, let destinationAddress else {
|
||||
throw NSError(domain: "findConnectionOwner", code: 0, userInfo: [
|
||||
NSLocalizedDescriptionKey: "Missing source or destination address",
|
||||
])
|
||||
}
|
||||
let owner = try RootHelperClient.shared.findConnectionOwner(
|
||||
ipProtocol: ipProtocol,
|
||||
sourceAddress: sourceAddress,
|
||||
sourcePort: sourcePort,
|
||||
destinationAddress: destinationAddress,
|
||||
destinationPort: destinationPort
|
||||
)
|
||||
let result = LibboxConnectionOwner()
|
||||
result.userId = owner.userId
|
||||
result.userName = owner.userName
|
||||
result.processPath = owner.processPath
|
||||
return result
|
||||
}
|
||||
#endif
|
||||
throw NSError(domain: "ExtensionPlatformInterface", code: 0, userInfo: [NSLocalizedDescriptionKey: String(localized: "Not implemented")])
|
||||
}
|
||||
|
||||
public func useProcFS() -> Bool {
|
||||
@@ -264,7 +280,7 @@ public class ExtensionPlatformInterface: NSObject, LibboxPlatformInterfaceProtoc
|
||||
|
||||
public func getInterfaces() throws -> LibboxNetworkInterfaceIteratorProtocol {
|
||||
guard let nwMonitor else {
|
||||
throw NSError(domain: "NWMonitor not started", code: 0)
|
||||
throw NSError(domain: "ExtensionPlatformInterface", code: 0, userInfo: [NSLocalizedDescriptionKey: String(localized: "NWMonitor not started")])
|
||||
}
|
||||
let path = nwMonitor.currentPath
|
||||
if path.status == .unsatisfied {
|
||||
@@ -313,10 +329,10 @@ public class ExtensionPlatformInterface: NSObject, LibboxPlatformInterfaceProtoc
|
||||
}
|
||||
|
||||
public func includeAllNetworks() -> Bool {
|
||||
#if !os(tvOS)
|
||||
return SharedPreferences.includeAllNetworks.getBlocking()
|
||||
#else
|
||||
#if os(tvOS)
|
||||
return false
|
||||
#else
|
||||
return tunnel.overridePreferences?.includeAllNetworks ?? false
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -324,12 +340,20 @@ public class ExtensionPlatformInterface: NSObject, LibboxPlatformInterfaceProtoc
|
||||
guard let networkSettings else {
|
||||
return
|
||||
}
|
||||
tunnel.reasserting = true
|
||||
tunnel.setTunnelNetworkSettings(nil) { _ in
|
||||
runBlocking {
|
||||
self.tunnel.reasserting = true
|
||||
defer { self.tunnel.reasserting = false }
|
||||
await withCheckedContinuation { continuation in
|
||||
self.tunnel.setTunnelNetworkSettings(nil) { _ in
|
||||
continuation.resume()
|
||||
}
|
||||
}
|
||||
await withCheckedContinuation { continuation in
|
||||
self.tunnel.setTunnelNetworkSettings(networkSettings) { _ in
|
||||
continuation.resume()
|
||||
}
|
||||
}
|
||||
}
|
||||
tunnel.setTunnelNetworkSettings(networkSettings) { _ in
|
||||
}
|
||||
tunnel.reasserting = false
|
||||
}
|
||||
|
||||
public func readWIFIState() -> LibboxWIFIState? {
|
||||
@@ -342,6 +366,9 @@ public class ExtensionPlatformInterface: NSObject, LibboxPlatformInterfaceProtoc
|
||||
}
|
||||
return LibboxWIFIState(network.ssid, wifiBSSID: network.bssid)!
|
||||
#elseif os(macOS)
|
||||
if Variant.useSystemExtension {
|
||||
return UserServiceClient.shared.readWIFIState()
|
||||
}
|
||||
guard let interface = CWWiFiClient.shared().interface() else {
|
||||
return nil
|
||||
}
|
||||
@@ -425,6 +452,8 @@ public class ExtensionPlatformInterface: NSObject, LibboxPlatformInterfaceProtoc
|
||||
|
||||
func reset() {
|
||||
networkSettings = nil
|
||||
nwMonitor?.cancel()
|
||||
nwMonitor = nil
|
||||
}
|
||||
|
||||
public func send(_ notification: LibboxNotification?) throws {
|
||||
@@ -432,6 +461,12 @@ public class ExtensionPlatformInterface: NSObject, LibboxPlatformInterfaceProtoc
|
||||
guard let notification else {
|
||||
return
|
||||
}
|
||||
#if os(macOS)
|
||||
if Variant.useSystemExtension {
|
||||
try UserServiceClient.shared.sendNotification(notification)
|
||||
return
|
||||
}
|
||||
#endif
|
||||
let center = UNUserNotificationCenter.current()
|
||||
let content = UNMutableNotificationContent()
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import Foundation
|
||||
import Libbox
|
||||
import NetworkExtension
|
||||
import os
|
||||
|
||||
private let logger = Logger(category: "ExtensionProfile")
|
||||
|
||||
@MainActor
|
||||
public class ExtensionProfile: ObservableObject {
|
||||
@@ -24,17 +27,19 @@ public class ExtensionProfile: ObservableObject {
|
||||
observer = NotificationCenter.default.addObserver(
|
||||
forName: NSNotification.Name.NEVPNStatusDidChange,
|
||||
object: manager.connection,
|
||||
queue: .main
|
||||
queue: nil
|
||||
) { [weak self] notification in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
guard let connection = notification.object as? NEVPNConnection else {
|
||||
return
|
||||
}
|
||||
self.connection = connection
|
||||
self.status = connection.status
|
||||
self.connectedDate = connection.connectedDate
|
||||
Task { @MainActor in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.connection = connection
|
||||
self.status = connection.status
|
||||
self.connectedDate = connection.connectedDate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,7 +83,7 @@ public class ExtensionProfile: ObservableObject {
|
||||
}
|
||||
|
||||
public func start() async throws {
|
||||
await fetchProfile()
|
||||
try await fetchProfile()
|
||||
manager.isEnabled = true
|
||||
let alwaysOn = await SharedPreferences.alwaysOn.get()
|
||||
let onDemandEnabled = await SharedPreferences.onDemandEnabled.get()
|
||||
@@ -96,29 +101,73 @@ public class ExtensionProfile: ObservableObject {
|
||||
}
|
||||
#endif
|
||||
try await manager.saveToPreferences()
|
||||
#if os(macOS)
|
||||
if Variant.useSystemExtension {
|
||||
try manager.connection.startVPNTunnel(options: [
|
||||
"username": NSString(string: NSUserName()),
|
||||
"manualStart": NSNumber(value: true),
|
||||
])
|
||||
return
|
||||
}
|
||||
#endif
|
||||
try manager.connection.startVPNTunnel(options: [
|
||||
"manualStart": NSNumber(value: true),
|
||||
])
|
||||
let options = try await prepareStartOptions()
|
||||
try manager.connection.startVPNTunnel(options: options)
|
||||
}
|
||||
|
||||
public func fetchProfile() async {
|
||||
do {
|
||||
if let profile = try await ProfileManager.get(Int64(SharedPreferences.selectedProfileID.get())) {
|
||||
if profile.type == .icloud {
|
||||
_ = try profile.read()
|
||||
public func reloadService() async throws {
|
||||
let options = try await prepareStartOptions()
|
||||
let data = try ExtensionStartOptions.encode(options)
|
||||
guard let session = connection as? NETunnelProviderSession else {
|
||||
throw NSError(domain: "ExtensionStartOptions", code: -1, userInfo: [
|
||||
NSLocalizedDescriptionKey: "Tunnel session unavailable",
|
||||
])
|
||||
}
|
||||
let response = try await withCheckedThrowingContinuation { continuation in
|
||||
do {
|
||||
try session.sendProviderMessage(data) { response in
|
||||
continuation.resume(returning: response)
|
||||
}
|
||||
} catch {
|
||||
continuation.resume(throwing: error)
|
||||
}
|
||||
}
|
||||
if let response, !response.isEmpty {
|
||||
let message = String(data: response, encoding: .utf8) ?? "Unknown error"
|
||||
throw NSError(domain: "ExtensionStartOptions", code: -1, userInfo: [
|
||||
NSLocalizedDescriptionKey: message,
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
private func prepareStartOptions() async throws -> [String: NSObject] {
|
||||
var options: [String: NSObject] = [
|
||||
"manualStart": NSNumber(value: true),
|
||||
]
|
||||
|
||||
let profileID = await SharedPreferences.selectedProfileID.get()
|
||||
guard let profile = try await ProfileManager.get(profileID) else {
|
||||
throw NSError(domain: "ExtensionProfile", code: -1, userInfo: [
|
||||
NSLocalizedDescriptionKey: "Missing selected profile",
|
||||
])
|
||||
}
|
||||
|
||||
let configContent = try profile.read()
|
||||
options["configContent"] = NSString(string: configContent)
|
||||
|
||||
options["ignoreMemoryLimit"] = await NSNumber(value: SharedPreferences.ignoreMemoryLimit.get())
|
||||
options["systemProxyEnabled"] = await NSNumber(value: SharedPreferences.systemProxyEnabled.get())
|
||||
options["excludeDefaultRoute"] = await NSNumber(value: SharedPreferences.excludeDefaultRoute.get())
|
||||
options["autoRouteUseSubRangesByDefault"] = await NSNumber(value: SharedPreferences.autoRouteUseSubRangesByDefault.get())
|
||||
options["excludeAPNsRoute"] = await NSNumber(value: SharedPreferences.excludeAPNsRoute.get())
|
||||
|
||||
#if !os(tvOS)
|
||||
options["includeAllNetworks"] = await NSNumber(value: SharedPreferences.includeAllNetworks.get())
|
||||
#endif
|
||||
|
||||
#if os(tvOS)
|
||||
options["commandServerPort"] = await NSNumber(value: SharedPreferences.commandServerPort.get())
|
||||
options["commandServerSecret"] = await NSString(string: SharedPreferences.commandServerSecret.get())
|
||||
#endif
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
public func fetchProfile() async throws {
|
||||
if let profile = try await ProfileManager.get(Int64(SharedPreferences.selectedProfileID.get())) {
|
||||
if profile.type == .icloud {
|
||||
_ = try profile.read()
|
||||
}
|
||||
} catch {
|
||||
NSLog("fetchProfile error: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,7 +179,7 @@ public class ExtensionProfile: ObservableObject {
|
||||
do {
|
||||
try LibboxNewStandaloneCommandClient()!.serviceClose()
|
||||
} catch {
|
||||
NSLog("serviceClose error: \(error.localizedDescription)")
|
||||
logger.debug("serviceClose error: \(error.localizedDescription)")
|
||||
}
|
||||
manager.connection.stopVPNTunnel()
|
||||
}
|
||||
@@ -142,7 +191,7 @@ public class ExtensionProfile: ObservableObject {
|
||||
try await Task.sleep(nanoseconds: NSEC_PER_SEC)
|
||||
waitSeconds += 1
|
||||
if waitSeconds >= 5 {
|
||||
throw NSError(domain: "Restart service timeout", code: 0)
|
||||
throw NSError(domain: "ExtensionProfile", code: 0, userInfo: [NSLocalizedDescriptionKey: String(localized: "Restart service timeout")])
|
||||
}
|
||||
}
|
||||
try await start()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import Foundation
|
||||
import Libbox
|
||||
import NetworkExtension
|
||||
import os.log
|
||||
#if os(iOS)
|
||||
import WidgetKit
|
||||
#endif
|
||||
@@ -9,20 +10,108 @@ import NetworkExtension
|
||||
#endif
|
||||
|
||||
open class ExtensionProvider: NEPacketTunnelProvider {
|
||||
public var username: String?
|
||||
private var commandServer: LibboxCommandServer!
|
||||
private static let logger = Logger(category: "ExtensionProvider")
|
||||
|
||||
public private(set) var commandServer: LibboxCommandServer?
|
||||
private var platformInterface: ExtensionPlatformInterface!
|
||||
public var tunnelOptions: [String: NSObject]?
|
||||
private var startOptionsURL: URL?
|
||||
|
||||
public struct OverridePreferences {
|
||||
public var includeAllNetworks: Bool = false
|
||||
public var systemProxyEnabled: Bool = true
|
||||
public var excludeDefaultRoute: Bool = false
|
||||
public var autoRouteUseSubRangesByDefault: Bool = false
|
||||
public var excludeAPNsRoute: Bool = false
|
||||
}
|
||||
|
||||
public var overridePreferences: OverridePreferences?
|
||||
|
||||
private func applyStartOptions(_ options: [String: NSObject]) {
|
||||
tunnelOptions = options
|
||||
var prefs = OverridePreferences()
|
||||
prefs.includeAllNetworks = (options["includeAllNetworks"] as? NSNumber)?.boolValue ?? false
|
||||
prefs.systemProxyEnabled = (options["systemProxyEnabled"] as? NSNumber)?.boolValue ?? true
|
||||
prefs.excludeDefaultRoute = (options["excludeDefaultRoute"] as? NSNumber)?.boolValue ?? false
|
||||
prefs.autoRouteUseSubRangesByDefault = (options["autoRouteUseSubRangesByDefault"] as? NSNumber)?.boolValue ?? false
|
||||
prefs.excludeAPNsRoute = (options["excludeAPNsRoute"] as? NSNumber)?.boolValue ?? false
|
||||
overridePreferences = prefs
|
||||
}
|
||||
|
||||
private func persistStartOptions(_ options: [String: NSObject]) throws {
|
||||
guard let startOptionsURL else {
|
||||
return
|
||||
}
|
||||
let data = try ExtensionStartOptions.encode(options)
|
||||
try data.write(to: startOptionsURL, options: .atomic)
|
||||
}
|
||||
|
||||
#if os(macOS)
|
||||
private var xpcListener: NSXPCListener?
|
||||
private var xpcService: CommandXPCService?
|
||||
private var locationManager: CLLocationManager?
|
||||
private var locationDelegate: stubLocationDelegate?
|
||||
#endif
|
||||
|
||||
override open func startTunnel(options startOptions: [String: NSObject]?) async throws {
|
||||
let basePath: String
|
||||
let workingPath: String
|
||||
let tempPath: String
|
||||
|
||||
#if os(macOS)
|
||||
if Variant.useSystemExtension {
|
||||
let containerURL = FileManager.default.homeDirectoryForCurrentUser
|
||||
basePath = containerURL.path
|
||||
workingPath = containerURL.appendingPathComponent("Working").path
|
||||
tempPath = containerURL.appendingPathComponent("Temp").path
|
||||
} else {
|
||||
basePath = FilePath.sharedDirectory.relativePath
|
||||
workingPath = FilePath.workingDirectory.relativePath
|
||||
tempPath = FilePath.cacheDirectory.relativePath
|
||||
}
|
||||
#else
|
||||
basePath = FilePath.sharedDirectory.relativePath
|
||||
workingPath = FilePath.workingDirectory.relativePath
|
||||
tempPath = FilePath.cacheDirectory.relativePath
|
||||
#endif
|
||||
|
||||
startOptionsURL = URL(fileURLWithPath: basePath).appendingPathComponent(ExtensionStartOptions.snapshotFileName)
|
||||
var effectiveOptions = startOptions
|
||||
if let startOptions {
|
||||
do {
|
||||
try persistStartOptions(startOptions)
|
||||
} catch {
|
||||
throw ExtensionStartupError("(packet-tunnel) error: persist start options: \(error.localizedDescription)")
|
||||
}
|
||||
} else if let startOptionsURL, FileManager.default.fileExists(atPath: startOptionsURL.path) {
|
||||
do {
|
||||
let data = try Data(contentsOf: startOptionsURL)
|
||||
effectiveOptions = try ExtensionStartOptions.decode(data)
|
||||
} catch {
|
||||
throw ExtensionStartupError("(packet-tunnel) error: load start options: \(error.localizedDescription)")
|
||||
}
|
||||
} else {
|
||||
throw ExtensionStartupError("(packet-tunnel) error: missing start options")
|
||||
}
|
||||
|
||||
if let effectiveOptions {
|
||||
applyStartOptions(effectiveOptions)
|
||||
}
|
||||
|
||||
override open func startTunnel(options _: [String: NSObject]?) async throws {
|
||||
let options = LibboxSetupOptions()
|
||||
options.basePath = FilePath.sharedDirectory.relativePath
|
||||
options.workingPath = FilePath.workingDirectory.relativePath
|
||||
options.tempPath = FilePath.cacheDirectory.relativePath
|
||||
options.basePath = basePath
|
||||
options.workingPath = workingPath
|
||||
options.tempPath = tempPath
|
||||
|
||||
options.logMaxLines = 3000
|
||||
|
||||
#if os(tvOS)
|
||||
options.commandServerListenPort = await SharedPreferences.commandServerPort.get()
|
||||
options.commandServerSecret = await SharedPreferences.commandServerSecret.get()
|
||||
if let port = effectiveOptions?["commandServerPort"] as? NSNumber {
|
||||
options.commandServerListenPort = port.int32Value
|
||||
}
|
||||
if let secret = effectiveOptions?["commandServerSecret"] as? String {
|
||||
options.commandServerSecret = secret
|
||||
}
|
||||
#endif
|
||||
|
||||
var setupError: NSError?
|
||||
@@ -31,13 +120,8 @@ open class ExtensionProvider: NEPacketTunnelProvider {
|
||||
throw ExtensionStartupError("(packet-tunnel) error: setup service: \(setupError.localizedDescription)")
|
||||
}
|
||||
|
||||
var stderrError: NSError?
|
||||
LibboxRedirectStderr(FilePath.cacheDirectory.appendingPathComponent("stderr.log").relativePath, &stderrError)
|
||||
if let stderrError {
|
||||
throw ExtensionStartupError("(packet-tunnel) redirect stderr error: \(stderrError.localizedDescription)")
|
||||
}
|
||||
|
||||
await LibboxSetMemoryLimit(!SharedPreferences.ignoreMemoryLimit.get())
|
||||
let ignoreMemoryLimit = (effectiveOptions?["ignoreMemoryLimit"] as? NSNumber)?.boolValue ?? false
|
||||
LibboxSetMemoryLimit(!ignoreMemoryLimit)
|
||||
|
||||
if platformInterface == nil {
|
||||
platformInterface = ExtensionPlatformInterface(self)
|
||||
@@ -48,12 +132,31 @@ open class ExtensionProvider: NEPacketTunnelProvider {
|
||||
throw ExtensionStartupError("(packet-tunnel): create command server error: \(error.localizedDescription)")
|
||||
}
|
||||
do {
|
||||
try commandServer.start()
|
||||
try commandServer!.start()
|
||||
} catch {
|
||||
throw ExtensionStartupError("(packet-tunnel): start command server error: \(error.localizedDescription)")
|
||||
}
|
||||
|
||||
#if os(macOS)
|
||||
if Variant.useSystemExtension {
|
||||
let socketPath = options.basePath + "/command.sock"
|
||||
xpcService = CommandXPCService(socketPath: socketPath)
|
||||
let machServiceName = AppConfiguration.appGroupID + ".system"
|
||||
xpcListener = NSXPCListener(machServiceName: machServiceName)
|
||||
xpcListener!.delegate = xpcService
|
||||
xpcListener!.resume()
|
||||
Self.logger.info("set Command Server")
|
||||
xpcService!.commandServer = commandServer
|
||||
}
|
||||
#endif
|
||||
|
||||
writeMessage("(packet-tunnel): Here I stand")
|
||||
try await startService()
|
||||
#if os(macOS)
|
||||
if Variant.useSystemExtension {
|
||||
xpcService!.markServiceReady()
|
||||
}
|
||||
#endif
|
||||
#if os(iOS)
|
||||
if #available(iOS 18.0, *) {
|
||||
ControlCenter.shared.reloadControls(ofKind: ExtensionProfile.controlKind)
|
||||
@@ -68,48 +171,28 @@ open class ExtensionProvider: NEPacketTunnelProvider {
|
||||
}
|
||||
|
||||
private func startService() async throws {
|
||||
let profileID = await SharedPreferences.selectedProfileID.get()
|
||||
let profile: Profile?
|
||||
do {
|
||||
profile = try await ProfileManager.get(profileID)
|
||||
} catch {
|
||||
throw ExtensionStartupError("(packet-tunnel) error: read selected profile: \(error.localizedDescription)")
|
||||
}
|
||||
guard let profile else {
|
||||
throw ExtensionStartupError("(packet-tunnel) error: missing selected profile")
|
||||
}
|
||||
let configContent: String
|
||||
do {
|
||||
configContent = try profile.read()
|
||||
} catch {
|
||||
throw ExtensionStartupError("(packet-tunnel) error: read config file \(profile.path): \(error.localizedDescription)")
|
||||
guard let configContent = tunnelOptions?["configContent"] as? String else {
|
||||
throw ExtensionStartupError("(packet-tunnel) error: missing configContent in tunnel options")
|
||||
}
|
||||
|
||||
let options = LibboxOverrideOptions()
|
||||
do {
|
||||
try commandServer.startOrReloadService(configContent, options: options)
|
||||
try commandServer!.startOrReloadService(configContent, options: options)
|
||||
} catch {
|
||||
throw ExtensionStartupError("(packet-tunnel) error: start service: \(error.localizedDescription)")
|
||||
}
|
||||
#if os(macOS)
|
||||
await SharedPreferences.startedByUser.set(true)
|
||||
if commandServer.needWIFIState() {
|
||||
if !Variant.useSystemExtension {
|
||||
locationManager = CLLocationManager()
|
||||
locationDelegate = stubLocationDelegate()
|
||||
locationManager?.delegate = locationDelegate
|
||||
locationManager?.requestLocation()
|
||||
} else {
|
||||
writeMessage("(packet-tunnel) WIFI SSID and BSSID information is not currently available in the standalone version of SFM. We are working on resolving this issue.")
|
||||
}
|
||||
if !Variant.useSystemExtension, commandServer!.needWIFIState() {
|
||||
locationManager = CLLocationManager()
|
||||
locationDelegate = stubLocationDelegate()
|
||||
locationManager?.delegate = locationDelegate
|
||||
locationManager?.requestLocation()
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#if os(macOS)
|
||||
|
||||
private var locationManager: CLLocationManager?
|
||||
private var locationDelegate: stubLocationDelegate?
|
||||
|
||||
class stubLocationDelegate: NSObject, CLLocationManagerDelegate {
|
||||
func locationManagerDidChangeAuthorization(_: CLLocationManager) {}
|
||||
|
||||
@@ -122,9 +205,9 @@ open class ExtensionProvider: NEPacketTunnelProvider {
|
||||
|
||||
func stopService() {
|
||||
do {
|
||||
try commandServer.closeService()
|
||||
try commandServer?.closeService()
|
||||
} catch {
|
||||
writeMessage("(packet-tunnel) error: stop service: \(error.localizedDescription)")
|
||||
writeMessage("(packet-tunnel) stop service: \(error.localizedDescription)")
|
||||
}
|
||||
if let platformInterface {
|
||||
platformInterface.reset()
|
||||
@@ -149,9 +232,15 @@ open class ExtensionProvider: NEPacketTunnelProvider {
|
||||
commandServer = nil
|
||||
}
|
||||
#if os(macOS)
|
||||
if reason == .userInitiated {
|
||||
await SharedPreferences.startedByUser.set(reason == .userInitiated)
|
||||
if Variant.useSystemExtension {
|
||||
xpcListener?.invalidate()
|
||||
xpcListener = nil
|
||||
xpcService?.commandServer = nil
|
||||
xpcService = nil
|
||||
UserServiceEndpointRegistry.shared.clear()
|
||||
}
|
||||
locationManager = nil
|
||||
locationDelegate = nil
|
||||
#endif
|
||||
#if os(iOS)
|
||||
if #available(iOS 18.0, *) {
|
||||
@@ -161,7 +250,15 @@ open class ExtensionProvider: NEPacketTunnelProvider {
|
||||
}
|
||||
|
||||
override open func handleAppMessage(_ messageData: Data) async -> Data? {
|
||||
messageData
|
||||
do {
|
||||
let options = try ExtensionStartOptions.decode(messageData)
|
||||
applyStartOptions(options)
|
||||
try persistStartOptions(options)
|
||||
try await reloadService()
|
||||
return nil
|
||||
} catch {
|
||||
return error.localizedDescription.data(using: .utf8)
|
||||
}
|
||||
}
|
||||
|
||||
override open func sleep() async {
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import Foundation
|
||||
|
||||
enum ExtensionStartOptions {
|
||||
static let snapshotFileName = "start_options.plist"
|
||||
|
||||
static func encode(_ options: [String: NSObject]) throws -> Data {
|
||||
try PropertyListSerialization.data(fromPropertyList: options, format: .binary, options: 0)
|
||||
}
|
||||
|
||||
static func decode(_ data: Data) throws -> [String: NSObject] {
|
||||
let plist = try PropertyListSerialization.propertyList(from: data, options: [], format: nil)
|
||||
guard let options = plist as? [String: NSObject] else {
|
||||
throw NSError(domain: "ExtensionStartOptions", code: -1, userInfo: [
|
||||
NSLocalizedDescriptionKey: "Invalid start options payload",
|
||||
])
|
||||
}
|
||||
return options
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
#if os(macOS)
|
||||
import Foundation
|
||||
import ServiceManagement
|
||||
|
||||
public enum HelperServiceManager {
|
||||
private static var rootHelperService: SMAppService {
|
||||
SMAppService.daemon(plistName: "\(AppConfiguration.rootHelperBundleID).plist")
|
||||
}
|
||||
|
||||
public static var rootHelperStatus: SMAppService.Status {
|
||||
rootHelperService.status
|
||||
}
|
||||
|
||||
public static func registerRootHelper() throws {
|
||||
if rootHelperService.status == .enabled {
|
||||
try rootHelperService.unregister()
|
||||
}
|
||||
try rootHelperService.register()
|
||||
}
|
||||
|
||||
public static func unregisterRootHelper() throws {
|
||||
try rootHelperService.unregister()
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,221 @@
|
||||
#if os(macOS)
|
||||
import Foundation
|
||||
import os
|
||||
|
||||
private let logger = Logger(category: "RootHelperXPC")
|
||||
|
||||
@objc public class ConnectionOwnerResult: NSObject, NSSecureCoding {
|
||||
public static let supportsSecureCoding = true
|
||||
|
||||
@objc public var userId: Int32
|
||||
@objc public var userName: String
|
||||
@objc public var processPath: String
|
||||
|
||||
public init(userId: Int32, userName: String, processPath: String) {
|
||||
self.userId = userId
|
||||
self.userName = userName
|
||||
self.processPath = processPath
|
||||
}
|
||||
|
||||
public required init?(coder: NSCoder) {
|
||||
userId = coder.decodeInt32(forKey: "userId")
|
||||
userName = coder.decodeObject(of: NSString.self, forKey: "userName") as? String ?? ""
|
||||
processPath = coder.decodeObject(of: NSString.self, forKey: "processPath") as? String ?? ""
|
||||
}
|
||||
|
||||
public func encode(with coder: NSCoder) {
|
||||
coder.encode(userId, forKey: "userId")
|
||||
coder.encode(userName as NSString, forKey: "userName")
|
||||
coder.encode(processPath as NSString, forKey: "processPath")
|
||||
}
|
||||
}
|
||||
|
||||
@objc public protocol RootHelperProtocol {
|
||||
func findConnectionOwner(
|
||||
ipProtocol: Int32,
|
||||
sourceAddress: String,
|
||||
sourcePort: Int32,
|
||||
destinationAddress: String,
|
||||
destinationPort: Int32,
|
||||
reply: @escaping (ConnectionOwnerResult?, NSError?) -> Void
|
||||
)
|
||||
|
||||
func getWorkingDirectorySize(reply: @escaping (Int64, NSError?) -> Void)
|
||||
func cleanWorkingDirectory(reply: @escaping (NSError?) -> Void)
|
||||
}
|
||||
|
||||
public enum RootHelperXPC {
|
||||
public static func configureInterface(_ interface: NSXPCInterface) {
|
||||
let resultClasses = NSSet(array: [ConnectionOwnerResult.self, NSString.self]) as! Set<AnyHashable>
|
||||
interface.setClasses(
|
||||
resultClasses,
|
||||
for: #selector(RootHelperProtocol.findConnectionOwner(ipProtocol:sourceAddress:sourcePort:destinationAddress:destinationPort:reply:)),
|
||||
argumentIndex: 0,
|
||||
ofReply: true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public class RootHelperClient {
|
||||
public static let shared = RootHelperClient()
|
||||
|
||||
private var connection: NSXPCConnection?
|
||||
private let connectionLock = NSLock()
|
||||
|
||||
private init() {}
|
||||
|
||||
private func getConnection() -> NSXPCConnection {
|
||||
connectionLock.lock()
|
||||
defer { connectionLock.unlock() }
|
||||
|
||||
if let existing = connection {
|
||||
return existing
|
||||
}
|
||||
|
||||
let newConnection = NSXPCConnection(machServiceName: AppConfiguration.rootHelperMachService)
|
||||
|
||||
let remoteInterface = NSXPCInterface(with: RootHelperProtocol.self)
|
||||
RootHelperXPC.configureInterface(remoteInterface)
|
||||
newConnection.remoteObjectInterface = remoteInterface
|
||||
|
||||
newConnection.invalidationHandler = { [weak self] in
|
||||
guard let self else { return }
|
||||
connectionLock.lock()
|
||||
connection = nil
|
||||
connectionLock.unlock()
|
||||
}
|
||||
|
||||
newConnection.resume()
|
||||
connection = newConnection
|
||||
return newConnection
|
||||
}
|
||||
|
||||
private func performXPCCall<T>(
|
||||
_ operation: String,
|
||||
call: (RootHelperProtocol, @escaping (T?, NSError?) -> Void) -> Void
|
||||
) throws -> T {
|
||||
let semaphore = DispatchSemaphore(value: 0)
|
||||
var result: T?
|
||||
var resultError: NSError?
|
||||
|
||||
let conn = getConnection()
|
||||
guard let proxy = conn.remoteObjectProxyWithErrorHandler({ error in
|
||||
logger.error("\(operation) XPC error: \(error.localizedDescription)")
|
||||
resultError = error as NSError
|
||||
semaphore.signal()
|
||||
}) as? RootHelperProtocol else {
|
||||
connectionLock.lock()
|
||||
connection = nil
|
||||
connectionLock.unlock()
|
||||
conn.invalidate()
|
||||
throw NSError(domain: "RootHelper", code: -1, userInfo: [
|
||||
NSLocalizedDescriptionKey: "Failed to get RootHelper proxy",
|
||||
])
|
||||
}
|
||||
|
||||
call(proxy) { value, error in
|
||||
result = value
|
||||
resultError = error
|
||||
semaphore.signal()
|
||||
}
|
||||
|
||||
let timeout = DispatchTime.now() + .seconds(5)
|
||||
if semaphore.wait(timeout: timeout) == .timedOut {
|
||||
let error = NSError(domain: "RootHelper", code: -1, userInfo: [
|
||||
NSLocalizedDescriptionKey: "\(operation) request timeout",
|
||||
])
|
||||
logger.error("\(operation): timeout")
|
||||
throw error
|
||||
}
|
||||
|
||||
if let error = resultError {
|
||||
logger.error("\(operation) error: \(error.localizedDescription)")
|
||||
throw error
|
||||
}
|
||||
|
||||
guard let value = result else {
|
||||
let error = NSError(domain: "RootHelper", code: -1, userInfo: [
|
||||
NSLocalizedDescriptionKey: "\(operation) returned nil",
|
||||
])
|
||||
throw error
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
private func performXPCCallVoid(
|
||||
_ operation: String,
|
||||
call: (RootHelperProtocol, @escaping (NSError?) -> Void) -> Void
|
||||
) throws {
|
||||
let semaphore = DispatchSemaphore(value: 0)
|
||||
var resultError: NSError?
|
||||
|
||||
let conn = getConnection()
|
||||
guard let proxy = conn.remoteObjectProxyWithErrorHandler({ error in
|
||||
logger.error("\(operation) XPC error: \(error.localizedDescription)")
|
||||
resultError = error as NSError
|
||||
semaphore.signal()
|
||||
}) as? RootHelperProtocol else {
|
||||
connectionLock.lock()
|
||||
connection = nil
|
||||
connectionLock.unlock()
|
||||
conn.invalidate()
|
||||
throw NSError(domain: "RootHelper", code: -1, userInfo: [
|
||||
NSLocalizedDescriptionKey: "Failed to get RootHelper proxy",
|
||||
])
|
||||
}
|
||||
|
||||
call(proxy) { error in
|
||||
resultError = error
|
||||
semaphore.signal()
|
||||
}
|
||||
|
||||
let timeout = DispatchTime.now() + .seconds(5)
|
||||
if semaphore.wait(timeout: timeout) == .timedOut {
|
||||
let error = NSError(domain: "RootHelper", code: -1, userInfo: [
|
||||
NSLocalizedDescriptionKey: "\(operation) request timeout",
|
||||
])
|
||||
logger.error("\(operation): timeout")
|
||||
throw error
|
||||
}
|
||||
|
||||
if let error = resultError {
|
||||
logger.error("\(operation) error: \(error.localizedDescription)")
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
public func findConnectionOwner(
|
||||
ipProtocol: Int32,
|
||||
sourceAddress: String,
|
||||
sourcePort: Int32,
|
||||
destinationAddress: String,
|
||||
destinationPort: Int32
|
||||
) throws -> ConnectionOwnerResult {
|
||||
try performXPCCall("findConnectionOwner") { proxy, reply in
|
||||
proxy.findConnectionOwner(
|
||||
ipProtocol: ipProtocol,
|
||||
sourceAddress: sourceAddress,
|
||||
sourcePort: sourcePort,
|
||||
destinationAddress: destinationAddress,
|
||||
destinationPort: destinationPort,
|
||||
reply: reply
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public func getWorkingDirectorySize() throws -> Int64 {
|
||||
try performXPCCall("getWorkingDirectorySize") { proxy, reply in
|
||||
proxy.getWorkingDirectorySize { size, error in
|
||||
reply(size as Int64?, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func cleanWorkingDirectory() throws {
|
||||
try performXPCCallVoid("cleanWorkingDirectory") { proxy, reply in
|
||||
proxy.cleanWorkingDirectory(reply: reply)
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -1,7 +1,10 @@
|
||||
#if os(macOS)
|
||||
import Foundation
|
||||
import os
|
||||
import SystemExtensions
|
||||
|
||||
private let logger = Logger(category: "SystemExtension")
|
||||
|
||||
public class SystemExtension: NSObject, OSSystemExtensionRequestDelegate {
|
||||
private let forceUpdate: Bool
|
||||
private let inBackground: Bool
|
||||
@@ -26,10 +29,10 @@
|
||||
existing.bundleVersion == ext.bundleVersion,
|
||||
existing.bundleShortVersion == ext.bundleShortVersion
|
||||
{
|
||||
NSLog("Skip update system extension")
|
||||
logger.info("Skip update system extension")
|
||||
return .cancel
|
||||
} else {
|
||||
NSLog("Update system extension")
|
||||
logger.info("Update system extension")
|
||||
return .replace
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
#if os(macOS)
|
||||
import CoreWLAN
|
||||
import Dispatch
|
||||
import Foundation
|
||||
import os
|
||||
import UserNotifications
|
||||
|
||||
private let logger = Logger(category: "UserService")
|
||||
|
||||
public extension Notification.Name {
|
||||
static let extensionRequiresWIFIState = Notification.Name("extensionRequiresWIFIState")
|
||||
static let extensionRequiresHelperService = Notification.Name("extensionRequiresHelperService")
|
||||
static let navigateToSettingsPage = Notification.Name("navigateToSettingsPage")
|
||||
}
|
||||
|
||||
public final class UserServiceEndpointPublisher: NSObject, NSXPCListenerDelegate {
|
||||
public static let shared = UserServiceEndpointPublisher()
|
||||
|
||||
private var listener: NSXPCListener?
|
||||
private let exportedObject = UserServiceHandler()
|
||||
|
||||
public func start() {
|
||||
guard listener == nil else {
|
||||
return
|
||||
}
|
||||
let listener = NSXPCListener.anonymous()
|
||||
listener.delegate = self
|
||||
listener.resume()
|
||||
self.listener = listener
|
||||
registerEndpoint(listener.endpoint)
|
||||
}
|
||||
|
||||
public func stop() {
|
||||
if let listener {
|
||||
listener.invalidate()
|
||||
self.listener = nil
|
||||
}
|
||||
registerEndpoint(nil)
|
||||
}
|
||||
|
||||
public func refreshEndpointRegistration() {
|
||||
guard let listener else {
|
||||
return
|
||||
}
|
||||
registerEndpoint(listener.endpoint)
|
||||
}
|
||||
|
||||
public func listener(_: NSXPCListener, shouldAcceptNewConnection newConnection: NSXPCConnection) -> Bool {
|
||||
let allowedBundleIDs = [AppConfiguration.systemExtensionBundleID]
|
||||
guard XPCConnectionValidator.validateConnection(
|
||||
newConnection,
|
||||
teamID: AppConfiguration.teamID,
|
||||
allowedBundleIDs: allowedBundleIDs
|
||||
) else {
|
||||
let info = XPCConnectionValidator.getConnectionInfo(newConnection)
|
||||
logger.warning("Rejected XPC connection: pid=\(info.pid), bundleID=\(info.bundleID ?? "unknown"), teamID=\(info.teamID ?? "unknown")")
|
||||
return false
|
||||
}
|
||||
|
||||
newConnection.exportedInterface = NSXPCInterface(with: UserServiceProtocol.self)
|
||||
newConnection.exportedObject = exportedObject
|
||||
newConnection.resume()
|
||||
return true
|
||||
}
|
||||
|
||||
public func checkExtensionRequirements() {
|
||||
Task.detached {
|
||||
let machServiceName = AppConfiguration.appGroupID + ".system"
|
||||
let connection = NSXPCConnection(machServiceName: machServiceName)
|
||||
let remoteInterface = NSXPCInterface(with: CommandXPCProtocol.self)
|
||||
CommandXPC.configureInterface(remoteInterface)
|
||||
connection.remoteObjectInterface = remoteInterface
|
||||
connection.resume()
|
||||
|
||||
guard let proxy = connection.remoteObjectProxyWithErrorHandler({ error in
|
||||
logger.error("Extension requirements check error: \(error.localizedDescription)")
|
||||
connection.invalidate()
|
||||
}) as? CommandXPCProtocol else {
|
||||
connection.invalidate()
|
||||
return
|
||||
}
|
||||
|
||||
proxy.extensionRequirements { needWIFI, needProcess, error in
|
||||
if let error {
|
||||
logger.error("Extension requirements error: \(error.localizedDescription)")
|
||||
connection.invalidate()
|
||||
return
|
||||
}
|
||||
if needWIFI {
|
||||
Task { @MainActor in
|
||||
NotificationCenter.default.post(name: .extensionRequiresWIFIState, object: nil)
|
||||
}
|
||||
}
|
||||
if needProcess {
|
||||
Task { @MainActor in
|
||||
NotificationCenter.default.post(name: .extensionRequiresHelperService, object: nil)
|
||||
}
|
||||
}
|
||||
connection.invalidate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func registerEndpoint(_ endpoint: NSXPCListenerEndpoint?) {
|
||||
let machServiceName = AppConfiguration.appGroupID + ".system"
|
||||
let connection = NSXPCConnection(machServiceName: machServiceName)
|
||||
let remoteInterface = NSXPCInterface(with: CommandXPCProtocol.self)
|
||||
CommandXPC.configureInterface(remoteInterface)
|
||||
connection.remoteObjectInterface = remoteInterface
|
||||
connection.resume()
|
||||
|
||||
guard let proxy = connection.remoteObjectProxyWithErrorHandler({ error in
|
||||
logger.error("UserService registration error: \(error.localizedDescription)")
|
||||
connection.invalidate()
|
||||
}) as? CommandXPCProtocol else {
|
||||
connection.invalidate()
|
||||
return
|
||||
}
|
||||
|
||||
proxy.registerUserServiceEndpoint(endpoint) { error in
|
||||
if let error {
|
||||
logger.error("UserService register failed: \(error.localizedDescription)")
|
||||
}
|
||||
connection.invalidate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class UserServiceHandler: NSObject, UserServiceProtocol {
|
||||
func getWIFIState(reply: @escaping (String?, String?, NSError?) -> Void) {
|
||||
let client = CWWiFiClient.shared()
|
||||
guard let interface = client.interface() else {
|
||||
reply(nil, nil, nil)
|
||||
return
|
||||
}
|
||||
let ssid = interface.ssid()
|
||||
let bssid = interface.bssid()
|
||||
reply(ssid, bssid, nil)
|
||||
}
|
||||
|
||||
func sendNotification(
|
||||
identifier: String,
|
||||
typeName _: String,
|
||||
typeID _: Int32,
|
||||
title: String,
|
||||
subtitle: String,
|
||||
body: String,
|
||||
openURL: String,
|
||||
reply: @escaping (NSError?) -> Void
|
||||
) {
|
||||
Task {
|
||||
do {
|
||||
let center = UNUserNotificationCenter.current()
|
||||
|
||||
let settings = await center.notificationSettings()
|
||||
|
||||
if settings.authorizationStatus == .notDetermined {
|
||||
let granted = try await center.requestAuthorization(options: [.alert, .sound])
|
||||
if !granted {
|
||||
let error = NSError(domain: "UserService", code: -1, userInfo: [
|
||||
NSLocalizedDescriptionKey: "Notification permission denied",
|
||||
])
|
||||
logger.error("sendNotification error: \(error.localizedDescription)")
|
||||
reply(error)
|
||||
return
|
||||
}
|
||||
} else if settings.authorizationStatus == .denied {
|
||||
let error = NSError(domain: "UserService", code: -1, userInfo: [
|
||||
NSLocalizedDescriptionKey: "Notification permission denied",
|
||||
])
|
||||
logger.error("sendNotification error: \(error.localizedDescription)")
|
||||
reply(error)
|
||||
return
|
||||
}
|
||||
|
||||
let content = UNMutableNotificationContent()
|
||||
content.title = title
|
||||
if !subtitle.isEmpty {
|
||||
content.subtitle = subtitle
|
||||
}
|
||||
content.body = body
|
||||
content.sound = .default
|
||||
|
||||
if !openURL.isEmpty {
|
||||
content.userInfo["openURL"] = openURL
|
||||
}
|
||||
|
||||
let request = UNNotificationRequest(
|
||||
identifier: identifier,
|
||||
content: content,
|
||||
trigger: nil
|
||||
)
|
||||
|
||||
try await center.add(request)
|
||||
reply(nil)
|
||||
} catch {
|
||||
let nsError = error as NSError
|
||||
logger.error("sendNotification error: \(nsError.localizedDescription)")
|
||||
reply(nsError)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,28 @@
|
||||
#if os(macOS)
|
||||
import Foundation
|
||||
|
||||
final class UserServiceEndpointRegistry {
|
||||
static let shared = UserServiceEndpointRegistry()
|
||||
|
||||
private let lock = NSLock()
|
||||
private var endpoint: NSXPCListenerEndpoint?
|
||||
|
||||
func update(_ endpoint: NSXPCListenerEndpoint) {
|
||||
lock.lock()
|
||||
self.endpoint = endpoint
|
||||
lock.unlock()
|
||||
}
|
||||
|
||||
func clear() {
|
||||
lock.lock()
|
||||
endpoint = nil
|
||||
lock.unlock()
|
||||
}
|
||||
|
||||
func get() -> NSXPCListenerEndpoint? {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
return endpoint
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,159 @@
|
||||
#if os(macOS)
|
||||
import Foundation
|
||||
import Libbox
|
||||
import os
|
||||
|
||||
private let logger = Logger(category: "UserServiceXPC")
|
||||
|
||||
@objc public protocol UserServiceProtocol {
|
||||
func getWIFIState(reply: @escaping (String?, String?, NSError?) -> Void)
|
||||
func sendNotification(
|
||||
identifier: String,
|
||||
typeName: String,
|
||||
typeID: Int32,
|
||||
title: String,
|
||||
subtitle: String,
|
||||
body: String,
|
||||
openURL: String,
|
||||
reply: @escaping (NSError?) -> Void
|
||||
)
|
||||
}
|
||||
|
||||
public class UserServiceClient {
|
||||
public static let shared = UserServiceClient()
|
||||
|
||||
private var connection: NSXPCConnection?
|
||||
private let connectionLock = NSLock()
|
||||
|
||||
private init() {}
|
||||
|
||||
private func getConnection() -> NSXPCConnection? {
|
||||
connectionLock.lock()
|
||||
defer { connectionLock.unlock() }
|
||||
|
||||
if let existing = connection {
|
||||
return existing
|
||||
}
|
||||
|
||||
guard let endpoint = UserServiceEndpointRegistry.shared.get() else {
|
||||
logger.error("UserService endpoint unavailable")
|
||||
return nil
|
||||
}
|
||||
|
||||
let newConnection = NSXPCConnection(listenerEndpoint: endpoint)
|
||||
newConnection.remoteObjectInterface = NSXPCInterface(with: UserServiceProtocol.self)
|
||||
|
||||
newConnection.invalidationHandler = { [weak self] in
|
||||
guard let self else { return }
|
||||
connectionLock.lock()
|
||||
connection = nil
|
||||
connectionLock.unlock()
|
||||
}
|
||||
|
||||
newConnection.resume()
|
||||
connection = newConnection
|
||||
return newConnection
|
||||
}
|
||||
|
||||
private func getProxy() -> UserServiceProtocol? {
|
||||
guard let conn = getConnection() else {
|
||||
return nil
|
||||
}
|
||||
guard let proxy = conn.remoteObjectProxyWithErrorHandler { [weak self] error in
|
||||
guard let self else { return }
|
||||
logger.error("UserService XPC error: \(error.localizedDescription)")
|
||||
connectionLock.lock()
|
||||
connection = nil
|
||||
connectionLock.unlock()
|
||||
} as? UserServiceProtocol else {
|
||||
connectionLock.lock()
|
||||
connection = nil
|
||||
connectionLock.unlock()
|
||||
conn.invalidate()
|
||||
return nil
|
||||
}
|
||||
return proxy
|
||||
}
|
||||
|
||||
private func performXPCCallVoid(
|
||||
_ operation: String,
|
||||
call: (UserServiceProtocol, @escaping (NSError?) -> Void) -> Void
|
||||
) throws {
|
||||
let semaphore = DispatchSemaphore(value: 0)
|
||||
var resultError: NSError?
|
||||
|
||||
guard let proxy = getProxy() else {
|
||||
throw NSError(domain: "UserService", code: -1, userInfo: [
|
||||
NSLocalizedDescriptionKey: "UserService connection unavailable",
|
||||
])
|
||||
}
|
||||
|
||||
call(proxy) { error in
|
||||
resultError = error
|
||||
semaphore.signal()
|
||||
}
|
||||
|
||||
let deadline = DispatchTime.now() + .seconds(5)
|
||||
if semaphore.wait(timeout: deadline) == .timedOut {
|
||||
let error = NSError(domain: "UserService", code: -1, userInfo: [
|
||||
NSLocalizedDescriptionKey: "\(operation) request timeout",
|
||||
])
|
||||
logger.error("\(operation): timeout")
|
||||
throw error
|
||||
}
|
||||
|
||||
if let error = resultError {
|
||||
logger.error("\(operation) error: \(error.localizedDescription)")
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
public func readWIFIState() -> LibboxWIFIState? {
|
||||
let semaphore = DispatchSemaphore(value: 0)
|
||||
var resultSSID: String?
|
||||
var resultBSSID: String?
|
||||
|
||||
guard let proxy = getProxy() else {
|
||||
logger.error("readWIFIState: no UserService connection")
|
||||
return nil
|
||||
}
|
||||
|
||||
proxy.getWIFIState { ssid, bssid, error in
|
||||
if let error {
|
||||
logger.error("readWIFIState error: \(error.localizedDescription)")
|
||||
} else {
|
||||
resultSSID = ssid
|
||||
resultBSSID = bssid
|
||||
}
|
||||
semaphore.signal()
|
||||
}
|
||||
|
||||
let timeout = DispatchTime.now() + .seconds(5)
|
||||
if semaphore.wait(timeout: timeout) == .timedOut {
|
||||
logger.error("readWIFIState: timeout")
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let ssid = resultSSID, let bssid = resultBSSID else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return LibboxWIFIState(ssid, wifiBSSID: bssid)
|
||||
}
|
||||
|
||||
public func sendNotification(_ notification: LibboxNotification) throws {
|
||||
try performXPCCallVoid("sendNotification") { proxy, reply in
|
||||
proxy.sendNotification(
|
||||
identifier: notification.identifier,
|
||||
typeName: notification.typeName,
|
||||
typeID: notification.typeID,
|
||||
title: notification.title,
|
||||
subtitle: notification.subtitle,
|
||||
body: notification.body,
|
||||
openURL: notification.openURL,
|
||||
reply: reply
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,80 @@
|
||||
#if os(macOS)
|
||||
import Foundation
|
||||
import Security
|
||||
|
||||
public struct XPCConnectionInfo {
|
||||
public let pid: pid_t
|
||||
public let bundleID: String?
|
||||
public let teamID: String?
|
||||
}
|
||||
|
||||
public enum XPCConnectionValidator {
|
||||
private static func getSecCode(for connection: NSXPCConnection) -> SecCode? {
|
||||
let pid = connection.processIdentifier
|
||||
var code: SecCode?
|
||||
let attributes = [kSecGuestAttributePid: pid] as CFDictionary
|
||||
guard SecCodeCopyGuestWithAttributes(nil, attributes, [], &code) == errSecSuccess else {
|
||||
return nil
|
||||
}
|
||||
return code
|
||||
}
|
||||
|
||||
private static func getSigningInfo(_ code: SecCode) -> [String: Any]? {
|
||||
var staticCode: SecStaticCode?
|
||||
guard SecCodeCopyStaticCode(code, [], &staticCode) == errSecSuccess,
|
||||
let staticCode
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
|
||||
var info: CFDictionary?
|
||||
guard SecCodeCopySigningInformation(staticCode, [], &info) == errSecSuccess else {
|
||||
return nil
|
||||
}
|
||||
return info as? [String: Any]
|
||||
}
|
||||
|
||||
public static func getConnectionInfo(_ connection: NSXPCConnection) -> XPCConnectionInfo {
|
||||
let pid = connection.processIdentifier
|
||||
|
||||
guard let secCode = getSecCode(for: connection),
|
||||
let signingInfo = getSigningInfo(secCode)
|
||||
else {
|
||||
return XPCConnectionInfo(pid: pid, bundleID: nil, teamID: nil)
|
||||
}
|
||||
|
||||
let bundleID = signingInfo[kSecCodeInfoIdentifier as String] as? String
|
||||
let teamID = signingInfo[kSecCodeInfoTeamIdentifier as String] as? String
|
||||
|
||||
return XPCConnectionInfo(pid: pid, bundleID: bundleID, teamID: teamID)
|
||||
}
|
||||
|
||||
public static func validateConnection(
|
||||
_ connection: NSXPCConnection,
|
||||
teamID: String,
|
||||
allowedBundleIDs: [String]
|
||||
) -> Bool {
|
||||
guard let secCode = getSecCode(for: connection) else {
|
||||
return false
|
||||
}
|
||||
|
||||
let requirement = "anchor apple generic and certificate leaf[subject.OU] = \"\(teamID)\""
|
||||
var secRequirement: SecRequirement?
|
||||
guard SecRequirementCreateWithString(requirement as CFString, [], &secRequirement) == errSecSuccess,
|
||||
let req = secRequirement,
|
||||
SecCodeCheckValidity(secCode, [], req) == errSecSuccess
|
||||
else {
|
||||
return false
|
||||
}
|
||||
|
||||
guard let signingInfo = getSigningInfo(secCode),
|
||||
let bundleID = signingInfo[kSecCodeInfoIdentifier as String] as? String,
|
||||
allowedBundleIDs.contains(bundleID)
|
||||
else {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -15,6 +15,13 @@ public enum AppConfiguration {
|
||||
return value
|
||||
}()
|
||||
|
||||
public static var teamID: String {
|
||||
guard let dotIndex = appGroupID.firstIndex(of: ".") else {
|
||||
fatalError("Invalid appGroupID format: \(appGroupID)")
|
||||
}
|
||||
return String(appGroupID[..<dotIndex])
|
||||
}
|
||||
|
||||
public static var extensionBundleID: String { "\(packageName).extension" }
|
||||
public static var systemExtensionBundleID: String { "\(packageName).system" }
|
||||
public static var fileProviderDomainID: String { "\(packageName).workingdir" }
|
||||
@@ -22,4 +29,9 @@ public enum AppConfiguration {
|
||||
public static var profileUTType: String { "\(packageName).profile" }
|
||||
public static var backgroundTaskID: String { "\(packageName).update_profiles" }
|
||||
public static var iCloudContainerID: String { "iCloud.\(packageName)" }
|
||||
|
||||
#if os(macOS)
|
||||
public static var rootHelperBundleID: String { "\(packageName).helper" }
|
||||
public static var rootHelperMachService: String { "\(appGroupID).helper" }
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import Foundation
|
||||
import os
|
||||
|
||||
public extension Logger {
|
||||
init(category: String) {
|
||||
self.init(subsystem: Bundle.main.bundleIdentifier!, category: category)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user