Files
sing-box-for-apple/Library/Network/UserServiceEndpointPublisher.swift
T

205 lines
8.0 KiB
Swift

#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