import Foundation import Libbox import Library import Network #if canImport(UIKit) import UIKit #endif public class ProfileServer { private var listener: NWListener @available(iOS 16.0, macOS 13.0, *) public init() throws { listener = try NWListener(using: .applicationService) listener.service = NWListener.Service(applicationService: "sing-box:profile") listener.newConnectionHandler = { connection in connection.stateUpdateHandler = { state in if state == .ready { Task.detached { try await Task.sleep(nanoseconds: NSEC_PER_MSEC * 100) await ProfileConnection(connection).process() } } } connection.start(queue: .global()) } } public func start() { listener.start(queue: .global()) } public func cancel() { listener.cancel() } class ProfileConnection { private let connection: NWSocket #if os(iOS) private var backgroundTaskID: UIBackgroundTaskIdentifier = .invalid #endif init(_ connection: NWConnection) { self.connection = NWSocket(connection) } func process() async { #if os(iOS) beginBackgroundTask() defer { endBackgroundTask() } #endif do { try await writeProfilePreviewList() } catch { NSLog("profile server: write profile list: \(error.localizedDescription)") await writeError(error.localizedDescription) return } do { while true { let message = try await connection.read() try await processMessage(message) } } catch { NSLog("profile server: process connection: \(error.localizedDescription)") await writeError(error.localizedDescription) } } #if os(iOS) private func beginBackgroundTask() { backgroundTaskID = UIApplication.shared.beginBackgroundTask { [weak self] in NSLog("profile server: background task expiring, cleaning up connection") self?.connection.cancel() self?.endBackgroundTask() } if backgroundTaskID != .invalid { NSLog("profile server: background task started") } } private func endBackgroundTask() { guard backgroundTaskID != .invalid else { return } NSLog("profile server: background task ended") UIApplication.shared.endBackgroundTask(backgroundTaskID) backgroundTaskID = .invalid } #endif private func processMessage(_ data: Data) async throws { if data.count == 0 { return } let messageType = Int64(data[0]) switch messageType { case LibboxMessageTypeProfileContentRequest: try await processProfileContentRequest(data) default: throw NSError(domain: "ProfileServer", code: 0, userInfo: [NSLocalizedDescriptionKey: String(localized: "Unexpected message type \(messageType)")]) } } private func processProfileContentRequest(_ data: Data) async throws { var error: NSError? let request = LibboxDecodeProfileContentRequest(data, &error) if let error { throw error } let profile = try await ProfileManager.get(request!.profileID) guard let profile else { throw NSError(domain: "ProfileServer", code: 0, userInfo: [NSLocalizedDescriptionKey: String(localized: "Profile not found")]) } let content = LibboxProfileContent() content.name = profile.name switch profile.type { case .local: content.type = LibboxProfileTypeLocal case .icloud: content.type = LibboxProfileTypeiCloud case .remote: content.type = LibboxProfileTypeRemote } content.config = try profile.read() if profile.type != .local { content.remotePath = profile.remoteURL! } if profile.type == .remote { content.autoUpdate = profile.autoUpdate content.autoUpdateInterval = profile.autoUpdateInterval if let lastUpdated = profile.lastUpdated { content.lastUpdated = Int64(lastUpdated.timeIntervalSince1970) } } try await connection.write(content.encode()) } private func writeProfilePreviewList() async throws { let profiles = try await ProfileManager.list() let encoder = LibboxProfileEncoder() for profile in profiles { let preview = LibboxProfilePreview() preview.profileID = profile.mustID preview.name = profile.name switch profile.type { case .local: preview.type = LibboxProfileTypeLocal case .icloud: preview.type = LibboxProfileTypeiCloud case .remote: preview.type = LibboxProfileTypeRemote } encoder.append(preview) } try await connection.write(encoder.encode()) } private func writeError(_ message: String) async { let errorMessage = LibboxErrorMessage() errorMessage.message = message try? await connection.write(errorMessage.encode()) } } }