refactor: Fix macOS standalone application
This commit is contained in:
@@ -100,7 +100,7 @@ public class ProfileServer {
|
|||||||
try await processProfileContentRequest(data)
|
try await processProfileContentRequest(data)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
throw NSError(domain: "unexpected message type \(messageType)", code: 0)
|
throw NSError(domain: "ProfileServer", code: 0, userInfo: [NSLocalizedDescriptionKey: String(localized: "Unexpected message type \(messageType)")])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,7 +113,7 @@ public class ProfileServer {
|
|||||||
|
|
||||||
let profile = try await ProfileManager.get(request!.profileID)
|
let profile = try await ProfileManager.get(request!.profileID)
|
||||||
guard let profile else {
|
guard let profile else {
|
||||||
throw NSError(domain: "profile not found", code: 0)
|
throw NSError(domain: "ProfileServer", code: 0, userInfo: [NSLocalizedDescriptionKey: String(localized: "Profile not found")])
|
||||||
}
|
}
|
||||||
let content = LibboxProfileContent()
|
let content = LibboxProfileContent()
|
||||||
content.name = profile.name
|
content.name = profile.name
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import Library
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !success {
|
if !success {
|
||||||
throw NSError(domain: "register task failed", code: 0)
|
throw NSError(domain: "UIProfileUpdateTask", code: 0, userInfo: [NSLocalizedDescriptionKey: String(localized: "Register task failed")])
|
||||||
}
|
}
|
||||||
registered = true
|
registered = true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,121 +1,8 @@
|
|||||||
|
import Library
|
||||||
|
@_exported import struct Library.AlertState
|
||||||
import SwiftUI
|
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 {
|
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
func alert(_ binding: Binding<AlertState?>, isLoading: Binding<Bool>) -> some View {
|
func alert(_ binding: Binding<AlertState?>, isLoading: Binding<Bool>) -> some View {
|
||||||
alert(
|
alert(
|
||||||
|
|||||||
@@ -153,3 +153,15 @@ public func FormNavigationLink(@ViewBuilder destination: () -> some View, @ViewB
|
|||||||
}, label: label)
|
}, label: label)
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if os(macOS)
|
||||||
|
public func FormNavigationLink(value: some Hashable, @ViewBuilder label: () -> some View) -> some View {
|
||||||
|
NavigationLink(value: value, label: label)
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension View {
|
||||||
|
func formNavigationDestination<D: Hashable>(for data: D.Type, @ViewBuilder destination: @escaping (D) -> some View) -> some View {
|
||||||
|
navigationDestination(for: data, destination: destination)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|||||||
@@ -0,0 +1,250 @@
|
|||||||
|
import Libbox
|
||||||
|
import Library
|
||||||
|
import NetworkExtension
|
||||||
|
import SwiftUI
|
||||||
|
#if os(macOS)
|
||||||
|
import CoreLocation
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if os(macOS)
|
||||||
|
@MainActor
|
||||||
|
private final class WIFIStateLocationManager: NSObject, ObservableObject, CLLocationManagerDelegate {
|
||||||
|
private let manager = CLLocationManager()
|
||||||
|
var onAuthorizationGranted: (() -> Void)?
|
||||||
|
private var pendingAuthorizationRequest = false
|
||||||
|
|
||||||
|
override init() {
|
||||||
|
super.init()
|
||||||
|
manager.delegate = self
|
||||||
|
}
|
||||||
|
|
||||||
|
func requestAuthorizationAndShowWarning() {
|
||||||
|
let status = manager.authorizationStatus
|
||||||
|
switch status {
|
||||||
|
case .notDetermined:
|
||||||
|
pendingAuthorizationRequest = true
|
||||||
|
manager.requestAlwaysAuthorization()
|
||||||
|
case .authorized, .authorizedAlways:
|
||||||
|
onAuthorizationGranted?()
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nonisolated func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
|
||||||
|
Task { @MainActor in
|
||||||
|
guard self.pendingAuthorizationRequest else { return }
|
||||||
|
self.pendingAuthorizationRequest = false
|
||||||
|
let status = manager.authorizationStatus
|
||||||
|
if status == .authorized || status == .authorizedAlways {
|
||||||
|
self.onAuthorizationGranted?()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
public struct GlobalChecksModifier: ViewModifier {
|
||||||
|
@EnvironmentObject private var environments: ExtensionEnvironments
|
||||||
|
@Environment(\.importProfile) private var importProfile
|
||||||
|
@Environment(\.importRemoteProfile) private var importRemoteProfile
|
||||||
|
@Environment(\.selection) private var selection
|
||||||
|
|
||||||
|
@State private var alert: AlertState?
|
||||||
|
@State private var notStarted = false
|
||||||
|
@Environment(\.openURL) private var openURL
|
||||||
|
|
||||||
|
#if os(macOS)
|
||||||
|
@StateObject private var wifiLocationManager = WIFIStateLocationManager()
|
||||||
|
#endif
|
||||||
|
|
||||||
|
public init() {}
|
||||||
|
|
||||||
|
public func body(content: Content) -> some View {
|
||||||
|
contentView(content)
|
||||||
|
.alert($alert)
|
||||||
|
.onAppear {
|
||||||
|
handleImportProfile()
|
||||||
|
handleImportRemoteProfile()
|
||||||
|
}
|
||||||
|
.onChangeCompat(of: importProfile.wrappedValue) { _ in
|
||||||
|
handleImportProfile()
|
||||||
|
}
|
||||||
|
.onChangeCompat(of: importRemoteProfile.wrappedValue) { _ in
|
||||||
|
handleImportRemoteProfile()
|
||||||
|
}
|
||||||
|
.onChangeCompat(of: environments.extensionProfile?.status) { status in
|
||||||
|
handleStatusChange(status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private func contentView(_ content: Content) -> some View {
|
||||||
|
#if os(macOS)
|
||||||
|
content
|
||||||
|
.onReceive(NotificationCenter.default.publisher(for: .extensionRequiresWIFIState)) { _ in
|
||||||
|
handleWiFiStateNotification()
|
||||||
|
}
|
||||||
|
.onReceive(NotificationCenter.default.publisher(for: .extensionRequiresHelperService)) { _ in
|
||||||
|
handleHelperServiceNotification()
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
content
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
private func handleImportProfile() {
|
||||||
|
guard let profile = importProfile.wrappedValue else { return }
|
||||||
|
importProfile.wrappedValue = nil
|
||||||
|
alert = AlertState(
|
||||||
|
title: String(localized: "Import Profile"),
|
||||||
|
message: String(localized: "Are you sure to import profile \(profile.name)?"),
|
||||||
|
primaryButton: .default(String(localized: "Import")) { [weak environments, selection] in
|
||||||
|
selection.wrappedValue = .dashboard
|
||||||
|
Task {
|
||||||
|
do {
|
||||||
|
try await profile.importProfile()
|
||||||
|
} catch {
|
||||||
|
await MainActor.run {
|
||||||
|
alert = AlertState(error: error)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
environments?.profileUpdate.send()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
secondaryButton: .cancel()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func handleImportRemoteProfile() {
|
||||||
|
guard let remoteProfile = importRemoteProfile.wrappedValue else { return }
|
||||||
|
importRemoteProfile.wrappedValue = nil
|
||||||
|
alert = AlertState(
|
||||||
|
title: String(localized: "Import Remote Profile"),
|
||||||
|
message: String(localized: "Are you sure to import remote profile \(remoteProfile.name)? You will connect to \(remoteProfile.host) to download the configuration."),
|
||||||
|
primaryButton: .default(String(localized: "Import")) { [weak environments, selection] in
|
||||||
|
selection.wrappedValue = .dashboard
|
||||||
|
environments?.pendingImportRemoteProfile = ImportRemoteProfileRequest(name: remoteProfile.name, url: remoteProfile.url)
|
||||||
|
},
|
||||||
|
secondaryButton: .cancel()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func handleStatusChange(_ status: NEVPNStatus?) {
|
||||||
|
guard let status else { return }
|
||||||
|
switch status {
|
||||||
|
case .connected:
|
||||||
|
notStarted = false
|
||||||
|
Task {
|
||||||
|
await checkDeprecatedNotes()
|
||||||
|
}
|
||||||
|
case .connecting:
|
||||||
|
notStarted = true
|
||||||
|
case .disconnected:
|
||||||
|
if #available(iOS 16.0, macOS 13.0, tvOS 17.0, *) {
|
||||||
|
if notStarted, let profile = environments.extensionProfile {
|
||||||
|
Task {
|
||||||
|
await checkLastDisconnectError(profile: profile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
notStarted = false
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(iOS 16.0, macOS 13.0, tvOS 17.0, *)
|
||||||
|
private nonisolated func checkLastDisconnectError(profile: ExtensionProfile) async {
|
||||||
|
if let alertState = await profile.checkLastDisconnectError() {
|
||||||
|
await MainActor.run {
|
||||||
|
alert = alertState
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private nonisolated func checkDeprecatedNotes() async {
|
||||||
|
let disableWarnings = await SharedPreferences.disableDeprecatedWarnings.get()
|
||||||
|
guard !disableWarnings else { return }
|
||||||
|
|
||||||
|
do {
|
||||||
|
let reports = try LibboxNewStandaloneCommandClient()!.getDeprecatedNotes()
|
||||||
|
if reports.hasNext() {
|
||||||
|
await MainActor.run {
|
||||||
|
showNextDeprecatedNote(reports)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
await MainActor.run {
|
||||||
|
alert = AlertState(error: error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func showNextDeprecatedNote(_ reports: any LibboxDeprecatedNoteIteratorProtocol) {
|
||||||
|
guard reports.hasNext() else { return }
|
||||||
|
|
||||||
|
let report = reports.next()!
|
||||||
|
let continueChain: () -> Void = {
|
||||||
|
Task.detached {
|
||||||
|
try? await Task.sleep(nanoseconds: 300 * NSEC_PER_MSEC)
|
||||||
|
await MainActor.run {
|
||||||
|
showNextDeprecatedNote(reports)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if report.migrationLink.isEmpty {
|
||||||
|
var state = AlertState(
|
||||||
|
title: String(localized: "Deprecated Warning"),
|
||||||
|
message: report.message(),
|
||||||
|
dismissButton: .cancel(String(localized: "Ok"))
|
||||||
|
)
|
||||||
|
state.onDismiss = continueChain
|
||||||
|
alert = state
|
||||||
|
} else {
|
||||||
|
alert = AlertState(
|
||||||
|
title: String(localized: "Deprecated Warning"),
|
||||||
|
message: report.message(),
|
||||||
|
primaryButton: .default(String(localized: "Documentation")) {
|
||||||
|
openURL(URL(string: report.migrationLink)!)
|
||||||
|
},
|
||||||
|
secondaryButton: .cancel(String(localized: "Ok")),
|
||||||
|
onDismiss: continueChain
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#if os(macOS)
|
||||||
|
private func handleWiFiStateNotification() {
|
||||||
|
guard Variant.useSystemExtension else { return }
|
||||||
|
wifiLocationManager.onAuthorizationGranted = {
|
||||||
|
alert = AlertState(
|
||||||
|
title: String(localized: "WiFi State Access"),
|
||||||
|
message: String(localized: "In the standalone version of SFM, reading WiFi state requires this app to be running. After you quit the SFM app, the sing-box service cannot continue to provide `wifi_ssid` and `wifi_bssid` routing rules.")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
wifiLocationManager.requestAuthorizationAndShowWarning()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func handleHelperServiceNotification() {
|
||||||
|
guard Variant.useSystemExtension, HelperServiceManager.rootHelperStatus != .enabled else { return }
|
||||||
|
alert = AlertState(
|
||||||
|
title: String(localized: "Helper Service Required"),
|
||||||
|
message: String(localized: "The sing-box service requires Helper Service to provide process lookup functionality, which supports `process_name` and `process_path` routing rules."),
|
||||||
|
primaryButton: .default(String(localized: "App Settings")) {
|
||||||
|
NotificationCenter.default.post(name: .navigateToSettingsPage, object: SettingsPage.app)
|
||||||
|
},
|
||||||
|
secondaryButton: .cancel()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension View {
|
||||||
|
func globalChecks() -> some View {
|
||||||
|
modifier(GlobalChecksModifier())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -495,10 +495,10 @@ extension ProfileCard {
|
|||||||
@Published var profileToEdit: Profile?
|
@Published var profileToEdit: Profile?
|
||||||
@Published var shareItemType: ShareItemType?
|
@Published var shareItemType: ShareItemType?
|
||||||
#if !os(tvOS)
|
#if !os(tvOS)
|
||||||
@Published var profileExportDocument: ProfileExportDocument?
|
@Published var profileExportDocument: ProfileExportDocument?
|
||||||
@Published var showProfileExporter = false
|
@Published var showProfileExporter = false
|
||||||
@Published var profileJSONExportDocument: ProfileJSONExportDocument?
|
@Published var profileJSONExportDocument: ProfileJSONExportDocument?
|
||||||
@Published var showJSONExporter = false
|
@Published var showJSONExporter = false
|
||||||
#endif
|
#endif
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
var shareButtonView: NSView?
|
var shareButtonView: NSView?
|
||||||
@@ -511,10 +511,7 @@ extension ProfileCard {
|
|||||||
try await profile.updateRemoteProfile()
|
try await profile.updateRemoteProfile()
|
||||||
environments.profileUpdate.send()
|
environments.profileUpdate.send()
|
||||||
} catch {
|
} catch {
|
||||||
alert = AlertState(
|
alert = AlertState(error: error)
|
||||||
title: String(localized: "Update Failed"),
|
|
||||||
message: error.localizedDescription
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -402,10 +402,7 @@ struct ProfilePickerSheet: View {
|
|||||||
try await profile.origin.updateRemoteProfile()
|
try await profile.origin.updateRemoteProfile()
|
||||||
environments.profileUpdate.send()
|
environments.profileUpdate.send()
|
||||||
} catch {
|
} catch {
|
||||||
alert = AlertState(
|
alert = AlertState(error: error)
|
||||||
title: String(localized: "Update Failed"),
|
|
||||||
message: error.localizedDescription
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,12 +4,8 @@ import SwiftUI
|
|||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
public struct DashboardView: View {
|
public struct DashboardView: View {
|
||||||
@Environment(\.openURL) private var openURL
|
|
||||||
@Environment(\.importProfile) private var importProfile
|
|
||||||
@Environment(\.importRemoteProfile) private var importRemoteProfile
|
|
||||||
@EnvironmentObject private var environments: ExtensionEnvironments
|
@EnvironmentObject private var environments: ExtensionEnvironments
|
||||||
@StateObject private var coordinator = DashboardViewModel()
|
@StateObject private var coordinator = DashboardViewModel()
|
||||||
@State private var importRemoteProfileRequest: NewProfileView.ImportRequest?
|
|
||||||
|
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
@Environment(\.controlActiveState) private var controlActiveState
|
@Environment(\.controlActiveState) private var controlActiveState
|
||||||
@@ -19,24 +15,16 @@ public struct DashboardView: View {
|
|||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
content
|
content
|
||||||
|
.alert($coordinator.alert)
|
||||||
.onAppear {
|
.onAppear {
|
||||||
coordinator.setOpenURL { openURL($0) }
|
|
||||||
coordinator.setEnvironments(environments)
|
coordinator.setEnvironments(environments)
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
Task { await coordinator.reload() }
|
Task { await coordinator.reload() }
|
||||||
#endif
|
#endif
|
||||||
handleImportProfile()
|
|
||||||
handleImportRemoteProfile()
|
|
||||||
}
|
|
||||||
.onChangeCompat(of: importProfile.wrappedValue) { _ in
|
|
||||||
handleImportProfile()
|
|
||||||
}
|
|
||||||
.onChangeCompat(of: importRemoteProfile.wrappedValue) { _ in
|
|
||||||
handleImportRemoteProfile()
|
|
||||||
}
|
}
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
.navigationDestination(item: $importRemoteProfileRequest) { request in
|
.navigationDestination(item: $environments.pendingImportRemoteProfile) { request in
|
||||||
NewProfileView(request)
|
NewProfileView(.init(name: request.name, url: request.url))
|
||||||
.environmentObject(environments)
|
.environmentObject(environments)
|
||||||
.onDisappear {
|
.onDisappear {
|
||||||
environments.profileUpdate.send()
|
environments.profileUpdate.send()
|
||||||
@@ -48,7 +36,7 @@ public struct DashboardView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
.sheet(item: $importRemoteProfileRequest) { request in
|
.sheet(item: $environments.pendingImportRemoteProfile) { request in
|
||||||
importRemoteProfileSheet(for: request)
|
importRemoteProfileSheet(for: request)
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@@ -60,48 +48,12 @@ public struct DashboardView: View {
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
private func handleImportProfile() {
|
|
||||||
if let profile = importProfile.wrappedValue {
|
|
||||||
importProfile.wrappedValue = nil
|
|
||||||
coordinator.alert = AlertState(
|
|
||||||
title: String(localized: "Import Profile"),
|
|
||||||
message: String(localized: "Are you sure to import profile \(profile.name)?"),
|
|
||||||
primaryButton: .default(String(localized: "Import")) {
|
|
||||||
Task {
|
|
||||||
do {
|
|
||||||
try await profile.importProfile()
|
|
||||||
} catch {
|
|
||||||
coordinator.alert = AlertState(error: error)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
environments.profileUpdate.send()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
secondaryButton: .cancel()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func handleImportRemoteProfile() {
|
|
||||||
if let remoteProfile = importRemoteProfile.wrappedValue {
|
|
||||||
importRemoteProfile.wrappedValue = nil
|
|
||||||
coordinator.alert = AlertState(
|
|
||||||
title: String(localized: "Import Remote Profile"),
|
|
||||||
message: String(localized: "Are you sure to import remote profile \(remoteProfile.name)? You will connect to \(remoteProfile.host) to download the configuration."),
|
|
||||||
primaryButton: .default(String(localized: "Import")) {
|
|
||||||
importRemoteProfileRequest = .init(name: remoteProfile.name, url: remoteProfile.url)
|
|
||||||
},
|
|
||||||
secondaryButton: .cancel()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private func importRemoteProfileSheet(for request: NewProfileView.ImportRequest) -> some View {
|
private func importRemoteProfileSheet(for request: ImportRemoteProfileRequest) -> some View {
|
||||||
NavigationSheet(title: "Import Profile", onDismiss: {
|
NavigationSheet(title: "Import Profile", onDismiss: {
|
||||||
environments.profileUpdate.send()
|
environments.profileUpdate.send()
|
||||||
}, content: {
|
}, content: {
|
||||||
NewProfileView(request)
|
NewProfileView(.init(name: request.name, url: request.url))
|
||||||
.environmentObject(environments)
|
.environmentObject(environments)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -132,9 +84,13 @@ public struct DashboardView: View {
|
|||||||
} else if let profile = environments.extensionProfile {
|
} else if let profile = environments.extensionProfile {
|
||||||
activeDashboardView
|
activeDashboardView
|
||||||
.environmentObject(profile)
|
.environmentObject(profile)
|
||||||
.alert($coordinator.alert)
|
|
||||||
.onChangeCompat(of: profile.status) { status in
|
.onChangeCompat(of: profile.status) { status in
|
||||||
coordinator.handleStatusChange(status, profile: profile)
|
#if os(macOS)
|
||||||
|
if Variant.useSystemExtension, status == .connected {
|
||||||
|
UserServiceEndpointPublisher.shared.refreshEndpointRegistration()
|
||||||
|
UserServiceEndpointPublisher.shared.checkExtensionRequirements()
|
||||||
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
FormView {
|
FormView {
|
||||||
|
|||||||
@@ -2,12 +2,15 @@ import Foundation
|
|||||||
import Libbox
|
import Libbox
|
||||||
import Library
|
import Library
|
||||||
import NetworkExtension
|
import NetworkExtension
|
||||||
|
import os
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
import AppKit
|
import AppKit
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
private let logger = Logger(category: "DashboardViewModel")
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
public final class DashboardViewModel: BaseViewModel {
|
public final class DashboardViewModel: BaseViewModel {
|
||||||
@Published public var profileList: [ProfilePreview] = []
|
@Published public var profileList: [ProfilePreview] = []
|
||||||
@@ -15,14 +18,12 @@ public final class DashboardViewModel: BaseViewModel {
|
|||||||
@Published public var selection = DashboardPage.overview
|
@Published public var selection = DashboardPage.overview
|
||||||
@Published public var systemProxyAvailable = false
|
@Published public var systemProxyAvailable = false
|
||||||
@Published public var systemProxyEnabled = false
|
@Published public var systemProxyEnabled = false
|
||||||
@Published public var notStarted = false
|
|
||||||
|
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
@Published public var systemExtensionInstalled = true
|
@Published public var systemExtensionInstalled = true
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
private weak var environments: ExtensionEnvironments?
|
private weak var environments: ExtensionEnvironments?
|
||||||
private var openURL: ((URL) -> Void)?
|
|
||||||
|
|
||||||
public func setEnvironments(_ environments: ExtensionEnvironments) {
|
public func setEnvironments(_ environments: ExtensionEnvironments) {
|
||||||
self.environments = environments
|
self.environments = environments
|
||||||
@@ -33,10 +34,6 @@ public final class DashboardViewModel: BaseViewModel {
|
|||||||
isLoading = true
|
isLoading = true
|
||||||
}
|
}
|
||||||
|
|
||||||
public func setOpenURL(_ openURL: @escaping (URL) -> Void) {
|
|
||||||
self.openURL = openURL
|
|
||||||
}
|
|
||||||
|
|
||||||
public func reload() async {
|
public func reload() async {
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
if Variant.useSystemExtension {
|
if Variant.useSystemExtension {
|
||||||
@@ -86,84 +83,13 @@ public final class DashboardViewModel: BaseViewModel {
|
|||||||
systemProxyAvailable = status.available
|
systemProxyAvailable = status.available
|
||||||
systemProxyEnabled = status.enabled
|
systemProxyEnabled = status.enabled
|
||||||
} catch {
|
} catch {
|
||||||
NSLog("reloadSystemProxy: \(error)")
|
logger.debug("reloadSystemProxy: \(error)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func updateSelectedProfile() async {
|
public func updateSelectedProfile() async {
|
||||||
selectedProfileID = await SharedPreferences.selectedProfileID.get()
|
selectedProfileID = await SharedPreferences.selectedProfileID.get()
|
||||||
}
|
}
|
||||||
|
|
||||||
public func handleStatusChange(_ status: NEVPNStatus, profile: ExtensionProfile) {
|
|
||||||
if status == .connected {
|
|
||||||
notStarted = false
|
|
||||||
Task { await checkDeprecatedNotes() }
|
|
||||||
} else if status == .connecting {
|
|
||||||
notStarted = true
|
|
||||||
} else if status == .disconnected {
|
|
||||||
if #available(iOS 16.0, macOS 13.0, tvOS 17.0, *) {
|
|
||||||
if notStarted {
|
|
||||||
Task { await checkLastDisconnectError(profile: profile) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
nonisolated func checkDeprecatedNotes() async {
|
|
||||||
let disableWarnings = await SharedPreferences.disableDeprecatedWarnings.get()
|
|
||||||
guard !disableWarnings else { return }
|
|
||||||
|
|
||||||
do {
|
|
||||||
let reports = try LibboxNewStandaloneCommandClient()!.getDeprecatedNotes()
|
|
||||||
if reports.hasNext() {
|
|
||||||
await MainActor.run {
|
|
||||||
loopShowDeprecateNotes(reports)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
NSLog("checkDeprecatedNotes: \(error)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func loopShowDeprecateNotes(_ reports: any LibboxDeprecatedNoteIteratorProtocol) {
|
|
||||||
guard reports.hasNext() else { return }
|
|
||||||
|
|
||||||
let report = reports.next()!
|
|
||||||
let continueChain: () -> Void = { [weak self] in
|
|
||||||
_ = Task.detached {
|
|
||||||
try? await Task.sleep(nanoseconds: 300 * NSEC_PER_MSEC)
|
|
||||||
await self?.loopShowDeprecateNotes(reports)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if report.migrationLink.isEmpty {
|
|
||||||
alert = AlertState(
|
|
||||||
title: String(localized: "Deprecated Warning"),
|
|
||||||
message: report.message(),
|
|
||||||
dismissButton: .cancel(String(localized: "Ok"))
|
|
||||||
)
|
|
||||||
alert?.onDismiss = continueChain
|
|
||||||
} else {
|
|
||||||
alert = AlertState(
|
|
||||||
title: String(localized: "Deprecated Warning"),
|
|
||||||
message: report.message(),
|
|
||||||
primaryButton: .default(String(localized: "Documentation")) {
|
|
||||||
self.openURL?(URL(string: report.migrationLink)!)
|
|
||||||
},
|
|
||||||
secondaryButton: .cancel(String(localized: "Ok")),
|
|
||||||
onDismiss: continueChain
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@available(iOS 16.0, macOS 13.0, tvOS 17.0, *)
|
|
||||||
nonisolated func checkLastDisconnectError(profile: ExtensionProfile) async {
|
|
||||||
if let alertState = await profile.checkLastDisconnectError() {
|
|
||||||
await MainActor.run {
|
|
||||||
alert = alertState
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@available(iOS 16.0, macOS 13.0, tvOS 17.0, *)
|
@available(iOS 16.0, macOS 13.0, tvOS 17.0, *)
|
||||||
@@ -184,7 +110,7 @@ extension ExtensionProfile {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
return AlertState(title: String(localized: "Service Error"), message: nsError.localizedDescription)
|
return AlertState(error: nsError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ public final class OverviewViewModel: BaseViewModel {
|
|||||||
|
|
||||||
if profile.status.isConnected {
|
if profile.status.isConnected {
|
||||||
do {
|
do {
|
||||||
try await serviceReload()
|
try await profile.reloadService()
|
||||||
} catch {
|
} catch {
|
||||||
alert = AlertState(error: error)
|
alert = AlertState(error: error)
|
||||||
}
|
}
|
||||||
@@ -21,10 +21,6 @@ public final class OverviewViewModel: BaseViewModel {
|
|||||||
reasserting = false
|
reasserting = false
|
||||||
}
|
}
|
||||||
|
|
||||||
public nonisolated func serviceReload() async throws {
|
|
||||||
try LibboxNewStandaloneCommandClient()!.serviceReload()
|
|
||||||
}
|
|
||||||
|
|
||||||
public nonisolated func setSystemProxyEnabled(_ enabled: Bool, profile: ExtensionProfile) async {
|
public nonisolated func setSystemProxyEnabled(_ enabled: Bool, profile: ExtensionProfile) async {
|
||||||
do {
|
do {
|
||||||
await SharedPreferences.systemProxyEnabled.set(enabled)
|
await SharedPreferences.systemProxyEnabled.set(enabled)
|
||||||
|
|||||||
@@ -86,7 +86,9 @@ private struct LogViewContent: View {
|
|||||||
let button = UIButton(type: .system)
|
let button = UIButton(type: .system)
|
||||||
let config = UIImage.SymbolConfiguration(scale: .large)
|
let config = UIImage.SymbolConfiguration(scale: .large)
|
||||||
button.setImage(UIImage(systemName: "line.3.horizontal.circle", withConfiguration: config), for: .normal)
|
button.setImage(UIImage(systemName: "line.3.horizontal.circle", withConfiguration: config), for: .normal)
|
||||||
button.tintColor = colorScheme == .dark ? .white : .black
|
if #available(iOS 17.0, *) {
|
||||||
|
button.tintColor = colorScheme == .dark ? .white : .black
|
||||||
|
}
|
||||||
button.showsMenuAsPrimaryAction = true
|
button.showsMenuAsPrimaryAction = true
|
||||||
button.menu = createMenu()
|
button.menu = createMenu()
|
||||||
button.setContentHuggingPriority(.required, for: .horizontal)
|
button.setContentHuggingPriority(.required, for: .horizontal)
|
||||||
@@ -96,7 +98,9 @@ private struct LogViewContent: View {
|
|||||||
|
|
||||||
func updateUIView(_ uiView: UIButton, context _: Context) {
|
func updateUIView(_ uiView: UIButton, context _: Context) {
|
||||||
uiView.menu = createMenu()
|
uiView.menu = createMenu()
|
||||||
uiView.tintColor = colorScheme == .dark ? .white : .black
|
if #available(iOS 17.0, *) {
|
||||||
|
uiView.tintColor = colorScheme == .dark ? .white : .black
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func createMenu() -> UIMenu {
|
private func createMenu() -> UIMenu {
|
||||||
|
|||||||
@@ -86,9 +86,11 @@ public class LogDataModel: ObservableObject {
|
|||||||
lastProcessedLogCount = 0
|
lastProcessedLogCount = 0
|
||||||
lastEffectiveLevel = nil
|
lastEffectiveLevel = nil
|
||||||
lastSearchText = ""
|
lastSearchText = ""
|
||||||
|
filteredLogs = []
|
||||||
|
visibleLogs = []
|
||||||
|
commandClient.clearLogs()
|
||||||
Task.detached {
|
Task.detached {
|
||||||
let client = LibboxNewStandaloneCommandClient()
|
try? LibboxNewStandaloneCommandClient()!.clearLogs()
|
||||||
try? client?.clearLogs()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,10 +39,10 @@ public final class EditProfileContentViewModel: BaseViewModel {
|
|||||||
|
|
||||||
private nonisolated func loadContentBackground() async throws {
|
private nonisolated func loadContentBackground() async throws {
|
||||||
guard let profileID else {
|
guard let profileID else {
|
||||||
throw NSError(domain: "Context destroyed", code: 0)
|
throw NSError(domain: "EditProfileContentViewModel", code: 0, userInfo: [NSLocalizedDescriptionKey: String(localized: "Context destroyed")])
|
||||||
}
|
}
|
||||||
guard let profile = try await ProfileManager.get(profileID) else {
|
guard let profile = try await ProfileManager.get(profileID) else {
|
||||||
throw NSError(domain: "Profile missing", code: 0)
|
throw NSError(domain: "EditProfileContentViewModel", code: 0, userInfo: [NSLocalizedDescriptionKey: String(localized: "Profile missing")])
|
||||||
}
|
}
|
||||||
let profileContent = try profile.read()
|
let profileContent = try profile.read()
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
|
|||||||
@@ -61,7 +61,7 @@
|
|||||||
do {
|
do {
|
||||||
message = try socket.read()
|
message = try socket.read()
|
||||||
} catch {
|
} catch {
|
||||||
throw NSError(domain: "read from connection: \(error.localizedDescription)", code: 0)
|
throw NSError(domain: "ImportProfileViewModel", code: 0, userInfo: [NSLocalizedDescriptionKey: String(localized: "Read from connection: \(error.localizedDescription)")])
|
||||||
}
|
}
|
||||||
var error: NSError?
|
var error: NSError?
|
||||||
switch Int64(message[0]) {
|
switch Int64(message[0]) {
|
||||||
@@ -71,7 +71,7 @@
|
|||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
if let message {
|
if let message {
|
||||||
throw NSError(domain: "remote error: \(message.message)", code: 0)
|
throw NSError(domain: "ImportProfileViewModel", code: 0, userInfo: [NSLocalizedDescriptionKey: String(localized: "Remote error: \(message.message)")])
|
||||||
}
|
}
|
||||||
case LibboxMessageTypeProfileList:
|
case LibboxMessageTypeProfileList:
|
||||||
let decoder = LibboxProfileDecoder()
|
let decoder = LibboxProfileDecoder()
|
||||||
@@ -97,7 +97,7 @@
|
|||||||
try await importProfile(content!, environments: environments)
|
try await importProfile(content!, environments: environments)
|
||||||
return
|
return
|
||||||
default:
|
default:
|
||||||
throw NSError(domain: "unknown message type \(message[0])", code: 0)
|
throw NSError(domain: "ImportProfileViewModel", code: 0, userInfo: [NSLocalizedDescriptionKey: String(localized: "Unknown message type \(message[0])")])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -243,17 +243,11 @@ public struct NewProfileMenuView: View {
|
|||||||
var error: NSError?
|
var error: NSError?
|
||||||
let remoteProfile = LibboxParseRemoteProfileImportLink(string, &error)
|
let remoteProfile = LibboxParseRemoteProfileImportLink(string, &error)
|
||||||
if let error {
|
if let error {
|
||||||
alert = AlertState(
|
alert = AlertState(error: error)
|
||||||
title: String(localized: "Invalid QR Code"),
|
|
||||||
message: error.localizedDescription
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
guard let remoteProfile else {
|
guard let remoteProfile else {
|
||||||
alert = AlertState(
|
alert = AlertState(errorMessage: String(localized: "The QR code does not contain a valid profile import link."))
|
||||||
title: String(localized: "Invalid QR Code"),
|
|
||||||
message: String(localized: "The QR code does not contain a valid profile import link.")
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
importRequest = NewProfileView.ImportRequest(name: remoteProfile.name, url: remoteProfile.url)
|
importRequest = NewProfileView.ImportRequest(name: remoteProfile.name, url: remoteProfile.url)
|
||||||
|
|||||||
@@ -111,10 +111,10 @@ public final class NewProfileViewModel: BaseViewModel {
|
|||||||
let profileConfig = profileConfigDirectory.appendingPathComponent("config_\(nextProfileID).json")
|
let profileConfig = profileConfigDirectory.appendingPathComponent("config_\(nextProfileID).json")
|
||||||
if fileImport {
|
if fileImport {
|
||||||
guard let fileURL else {
|
guard let fileURL else {
|
||||||
throw NSError(domain: "Missing file", code: 0)
|
throw NSError(domain: "NewProfileViewModel", code: 0, userInfo: [NSLocalizedDescriptionKey: String(localized: "Missing file")])
|
||||||
}
|
}
|
||||||
if !fileURL.startAccessingSecurityScopedResource() {
|
if !fileURL.startAccessingSecurityScopedResource() {
|
||||||
throw NSError(domain: "Missing access to selected file", code: 0)
|
throw NSError(domain: "NewProfileViewModel", code: 0, userInfo: [NSLocalizedDescriptionKey: String(localized: "Missing access to selected file")])
|
||||||
}
|
}
|
||||||
defer {
|
defer {
|
||||||
fileURL.stopAccessingSecurityScopedResource()
|
fileURL.stopAccessingSecurityScopedResource()
|
||||||
|
|||||||
@@ -48,54 +48,54 @@ public struct QRSDisplayView: View {
|
|||||||
Text(String(localized: "FPS"))
|
Text(String(localized: "FPS"))
|
||||||
Spacer()
|
Spacer()
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
Button {
|
Button {
|
||||||
fps = max(1, fps - 1)
|
fps = max(1, fps - 1)
|
||||||
} label: {
|
} label: {
|
||||||
Image(systemName: "minus")
|
Image(systemName: "minus")
|
||||||
}
|
}
|
||||||
Text(verbatim: "\(Int(fps))")
|
Text(verbatim: "\(Int(fps))")
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
.frame(minWidth: 50)
|
.frame(minWidth: 50)
|
||||||
Button {
|
Button {
|
||||||
fps = min(60, fps + 1)
|
fps = min(60, fps + 1)
|
||||||
} label: {
|
} label: {
|
||||||
Image(systemName: "plus")
|
Image(systemName: "plus")
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
Text(verbatim: "\(Int(fps))")
|
Text(verbatim: "\(Int(fps))")
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
#if !os(tvOS)
|
#if !os(tvOS)
|
||||||
Slider(value: $fps, in: 1 ... 60, step: 1)
|
Slider(value: $fps, in: 1 ... 60, step: 1)
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
HStack {
|
HStack {
|
||||||
Text(String(localized: "Slice Size"))
|
Text(String(localized: "Slice Size"))
|
||||||
Spacer()
|
Spacer()
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
Button {
|
Button {
|
||||||
sliceSize = max(100, sliceSize - 100)
|
sliceSize = max(100, sliceSize - 100)
|
||||||
} label: {
|
} label: {
|
||||||
Image(systemName: "minus")
|
Image(systemName: "minus")
|
||||||
}
|
}
|
||||||
Text("\(Int(sliceSize))")
|
Text("\(Int(sliceSize))")
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
.frame(minWidth: 50)
|
.frame(minWidth: 50)
|
||||||
Button {
|
Button {
|
||||||
sliceSize = min(1500, sliceSize + 100)
|
sliceSize = min(1500, sliceSize + 100)
|
||||||
} label: {
|
} label: {
|
||||||
Image(systemName: "plus")
|
Image(systemName: "plus")
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
Text("\(Int(sliceSize))")
|
Text("\(Int(sliceSize))")
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
#if !os(tvOS)
|
#if !os(tvOS)
|
||||||
Slider(value: $sliceSize, in: 100 ... 1500, step: 100)
|
Slider(value: $sliceSize, in: 100 ... 1500, step: 100)
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
.padding(.horizontal)
|
.padding(.horizontal)
|
||||||
@@ -130,9 +130,9 @@ public struct QRSDisplayView: View {
|
|||||||
.padding(.horizontal)
|
.padding(.horizontal)
|
||||||
}
|
}
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
.padding()
|
.padding()
|
||||||
#else
|
#else
|
||||||
.padding([.horizontal, .bottom])
|
.padding([.horizontal, .bottom])
|
||||||
#endif
|
#endif
|
||||||
.onAppear {
|
.onAppear {
|
||||||
setupGenerator()
|
setupGenerator()
|
||||||
|
|||||||
@@ -145,10 +145,7 @@
|
|||||||
message: String(localized: "Please enable camera access in Settings to scan QR codes.")
|
message: String(localized: "Please enable camera access in Settings to scan QR codes.")
|
||||||
)
|
)
|
||||||
default:
|
default:
|
||||||
alert = AlertState(
|
alert = AlertState(error: error)
|
||||||
title: String(localized: "Scanner Error"),
|
|
||||||
message: error.localizedDescription
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import SwiftUI
|
|||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
import FileProvider
|
import FileProvider
|
||||||
import UIKit
|
import UIKit
|
||||||
|
#elseif os(macOS)
|
||||||
|
import ServiceManagement
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
@@ -15,7 +17,11 @@ public struct CoreView: View {
|
|||||||
@State private var disableDeprecatedWarnings = false
|
@State private var disableDeprecatedWarnings = false
|
||||||
|
|
||||||
@State private var version = ""
|
@State private var version = ""
|
||||||
@State private var dataSize = ""
|
@State private var dataSize: String?
|
||||||
|
|
||||||
|
#if os(macOS)
|
||||||
|
@State private var helperUnavailable = false
|
||||||
|
#endif
|
||||||
|
|
||||||
public init() {}
|
public init() {}
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
@@ -29,7 +35,21 @@ public struct CoreView: View {
|
|||||||
} else {
|
} else {
|
||||||
FormView {
|
FormView {
|
||||||
FormTextItem("Version", version)
|
FormTextItem("Version", version)
|
||||||
FormTextItem("Data Size", dataSize)
|
if let dataSize {
|
||||||
|
FormTextItem("Data Size", dataSize)
|
||||||
|
} else {
|
||||||
|
#if os(macOS)
|
||||||
|
HStack {
|
||||||
|
Text("Data Size")
|
||||||
|
Spacer()
|
||||||
|
Text("Unavailable")
|
||||||
|
.foregroundStyle(.red)
|
||||||
|
.onTapGesture {
|
||||||
|
alert = helperRequiredAlert()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
if Variant.isBeta {
|
if Variant.isBeta {
|
||||||
Section {}
|
Section {}
|
||||||
@@ -40,10 +60,12 @@ public struct CoreView: View {
|
|||||||
|
|
||||||
Section("Working Directory") {
|
Section("Working Directory") {
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
FormButton {
|
if !Variant.useSystemExtension {
|
||||||
NSWorkspace.shared.selectFile(nil, inFileViewerRootedAtPath: FilePath.workingDirectory.relativePath)
|
FormButton {
|
||||||
} label: {
|
NSWorkspace.shared.selectFile(nil, inFileViewerRootedAtPath: FilePath.workingDirectory.relativePath)
|
||||||
Label("Open", systemImage: "macwindow.and.cursorarrow")
|
} label: {
|
||||||
|
Label("Open", systemImage: "macwindow.and.cursorarrow")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
#elseif os(iOS)
|
#elseif os(iOS)
|
||||||
if #available(iOS 16.0, *) {
|
if #available(iOS 16.0, *) {
|
||||||
@@ -85,7 +107,11 @@ public struct CoreView: View {
|
|||||||
} else {
|
} else {
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
version = LibboxVersion()
|
version = LibboxVersion()
|
||||||
dataSize = "Loading..."
|
#if os(macOS)
|
||||||
|
if Variant.useSystemExtension {
|
||||||
|
helperUnavailable = HelperServiceManager.rootHelperStatus != .enabled
|
||||||
|
}
|
||||||
|
#endif
|
||||||
isLoading = false
|
isLoading = false
|
||||||
}
|
}
|
||||||
await loadSettingsBackground()
|
await loadSettingsBackground()
|
||||||
@@ -94,7 +120,20 @@ public struct CoreView: View {
|
|||||||
|
|
||||||
private nonisolated func loadSettingsBackground() async {
|
private nonisolated func loadSettingsBackground() async {
|
||||||
let disableDeprecatedWarnings = await SharedPreferences.disableDeprecatedWarnings.get()
|
let disableDeprecatedWarnings = await SharedPreferences.disableDeprecatedWarnings.get()
|
||||||
let dataSize = (try? FilePath.workingDirectory.formattedSize()) ?? "Unknown"
|
let dataSize: String?
|
||||||
|
#if os(macOS)
|
||||||
|
if Variant.useSystemExtension {
|
||||||
|
if let size = try? RootHelperClient.shared.getWorkingDirectorySize() {
|
||||||
|
dataSize = LibboxFormatBytes(size)
|
||||||
|
} else {
|
||||||
|
dataSize = nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dataSize = (try? FilePath.workingDirectory.formattedSize()) ?? "Unknown"
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
dataSize = (try? FilePath.workingDirectory.formattedSize()) ?? "Unknown"
|
||||||
|
#endif
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
self.disableDeprecatedWarnings = disableDeprecatedWarnings
|
self.disableDeprecatedWarnings = disableDeprecatedWarnings
|
||||||
self.dataSize = dataSize
|
self.dataSize = dataSize
|
||||||
@@ -102,6 +141,12 @@ public struct CoreView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func confirmDestroyWorkingDirectory() async {
|
private func confirmDestroyWorkingDirectory() async {
|
||||||
|
#if os(macOS)
|
||||||
|
if helperUnavailable {
|
||||||
|
alert = helperRequiredAlert()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
#endif
|
||||||
if environments.extensionProfile?.status.isConnected == true {
|
if environments.extensionProfile?.status.isConnected == true {
|
||||||
alert = AlertState(
|
alert = AlertState(
|
||||||
title: String(localized: "Service is Running"),
|
title: String(localized: "Service is Running"),
|
||||||
@@ -119,17 +164,44 @@ public struct CoreView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func stopServiceAndDestroy() async {
|
private func stopServiceAndDestroy() async {
|
||||||
try? await environments.extensionProfile?.stop()
|
do {
|
||||||
await destroyWorkingDirectory()
|
try await environments.extensionProfile!.stop()
|
||||||
}
|
await destroyWorkingDirectory()
|
||||||
|
} catch {
|
||||||
private nonisolated func destroyWorkingDirectory() async {
|
alert = AlertState(error: error)
|
||||||
try? FileManager.default.removeItem(at: FilePath.workingDirectory)
|
|
||||||
await MainActor.run {
|
|
||||||
isLoading = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func destroyWorkingDirectory() async {
|
||||||
|
do {
|
||||||
|
#if os(macOS)
|
||||||
|
if Variant.useSystemExtension {
|
||||||
|
try RootHelperClient.shared.cleanWorkingDirectory()
|
||||||
|
} else {
|
||||||
|
try FileManager.default.removeItem(at: FilePath.workingDirectory)
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
try FileManager.default.removeItem(at: FilePath.workingDirectory)
|
||||||
|
#endif
|
||||||
|
isLoading = true
|
||||||
|
} catch {
|
||||||
|
alert = AlertState(error: error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#if os(macOS)
|
||||||
|
private func helperRequiredAlert() -> AlertState {
|
||||||
|
AlertState(
|
||||||
|
title: String(localized: "Helper Service Required"),
|
||||||
|
message: String(localized: "Managing working directory requires Helper Service."),
|
||||||
|
primaryButton: .default(String(localized: "App Settings")) {
|
||||||
|
NotificationCenter.default.post(name: .navigateToSettingsPage, object: SettingsPage.app)
|
||||||
|
},
|
||||||
|
secondaryButton: .cancel(String(localized: "Ok"))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
@available(iOS 16.0, *)
|
@available(iOS 16.0, *)
|
||||||
private nonisolated func openInFilesApp() async {
|
private nonisolated func openInFilesApp() async {
|
||||||
|
|||||||
@@ -12,6 +12,11 @@ public struct AppView: View {
|
|||||||
@State private var startAtLogin = false
|
@State private var startAtLogin = false
|
||||||
@Environment(\.showMenuBarExtra) private var showMenuBarExtra
|
@Environment(\.showMenuBarExtra) private var showMenuBarExtra
|
||||||
@State private var menuBarExtraInBackground = false
|
@State private var menuBarExtraInBackground = false
|
||||||
|
#if os(macOS)
|
||||||
|
|
||||||
|
@State private var rootHelperRegistrationStatus: SMAppService.Status = .notRegistered
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
@State private var alert: AlertState?
|
@State private var alert: AlertState?
|
||||||
|
|
||||||
@@ -67,6 +72,48 @@ public struct AppView: View {
|
|||||||
Label("Uninstall", systemImage: "trash.fill").foregroundColor(.red)
|
Label("Uninstall", systemImage: "trash.fill").foregroundColor(.red)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Section {
|
||||||
|
if rootHelperRegistrationStatus == .enabled {
|
||||||
|
FormButton {
|
||||||
|
performHelperAction {
|
||||||
|
try HelperServiceManager.unregisterRootHelper()
|
||||||
|
try HelperServiceManager.registerRootHelper()
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Label("Update", systemImage: "arrow.down.doc.fill")
|
||||||
|
}
|
||||||
|
FormButton(role: .destructive) {
|
||||||
|
performHelperAction {
|
||||||
|
try HelperServiceManager.unregisterRootHelper()
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Label("Uninstall", systemImage: "trash.fill").foregroundColor(.red)
|
||||||
|
}
|
||||||
|
} else if rootHelperRegistrationStatus == .requiresApproval {
|
||||||
|
FormButton {
|
||||||
|
openHelperSettings()
|
||||||
|
} label: {
|
||||||
|
Label("Enable", systemImage: "switch.2")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
FormButton {
|
||||||
|
performHelperAction {
|
||||||
|
try HelperServiceManager.registerRootHelper()
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Label("Install", systemImage: "square.and.arrow.down.fill")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} header: {
|
||||||
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
Text("Helper Service")
|
||||||
|
Text("This helper service provides process lookup for `process_name` and `process_path` routing rules, and manages the working directory.")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
.textCase(nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
@@ -83,6 +130,9 @@ public struct AppView: View {
|
|||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
startAtLogin = SMAppService.mainApp.status == .enabled
|
startAtLogin = SMAppService.mainApp.status == .enabled
|
||||||
menuBarExtraInBackground = await SharedPreferences.menuBarExtraInBackground.get()
|
menuBarExtraInBackground = await SharedPreferences.menuBarExtraInBackground.get()
|
||||||
|
if Variant.useSystemExtension {
|
||||||
|
refreshHelperStatus()
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
isLoading = false
|
isLoading = false
|
||||||
}
|
}
|
||||||
@@ -147,5 +197,31 @@ public struct AppView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func performHelperAction(_ action: () throws -> Void) {
|
||||||
|
do {
|
||||||
|
try action()
|
||||||
|
refreshHelperStatus()
|
||||||
|
} catch {
|
||||||
|
alert = AlertState(error: error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func refreshHelperStatus() {
|
||||||
|
rootHelperRegistrationStatus = HelperServiceManager.rootHelperStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
private func openHelperSettings() {
|
||||||
|
if #available(macOS 13.0, *) {
|
||||||
|
SMAppService.openSystemSettingsLoginItems()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if let url = URL(string: "x-apple.systempreferences:com.apple.preference.users?LoginItems"),
|
||||||
|
NSWorkspace.shared.open(url)
|
||||||
|
{
|
||||||
|
return
|
||||||
|
}
|
||||||
|
NSWorkspace.shared.open(URL(fileURLWithPath: "/System/Applications/System Preferences.app"))
|
||||||
|
}
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -296,7 +296,7 @@ public struct OnDemandRulesView: View {
|
|||||||
await SharedPreferences.onDemandRules.set(rules)
|
await SharedPreferences.onDemandRules.set(rules)
|
||||||
let savedRules = await SharedPreferences.onDemandRules.get()
|
let savedRules = await SharedPreferences.onDemandRules.get()
|
||||||
if savedRules != rules {
|
if savedRules != rules {
|
||||||
alert = AlertState(errorMessage: "Failed to save rules")
|
alert = AlertState(errorMessage: String(localized: "Failed to save rules"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
await updateService()
|
await updateService()
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ struct PacketTunnelView: View {
|
|||||||
Group {
|
Group {
|
||||||
if isLoading {
|
if isLoading {
|
||||||
ProgressView().onAppear {
|
ProgressView().onAppear {
|
||||||
Task.detached {
|
Task {
|
||||||
await loadSettings()
|
await loadSettings()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -115,6 +115,7 @@ struct PacketTunnelView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
private func loadSettings() async {
|
private func loadSettings() async {
|
||||||
ignoreMemoryLimit = await SharedPreferences.ignoreMemoryLimit.get()
|
ignoreMemoryLimit = await SharedPreferences.ignoreMemoryLimit.get()
|
||||||
#if !os(tvOS)
|
#if !os(tvOS)
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import Libbox
|
|
||||||
import Library
|
import Library
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
@@ -15,7 +14,7 @@ public struct ProfileOverrideView: View {
|
|||||||
Group {
|
Group {
|
||||||
if isLoading {
|
if isLoading {
|
||||||
ProgressView().onAppear {
|
ProgressView().onAppear {
|
||||||
Task.detached {
|
Task {
|
||||||
await loadSettings()
|
await loadSettings()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -64,12 +63,13 @@ public struct ProfileOverrideView: View {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
do {
|
do {
|
||||||
try LibboxNewStandaloneCommandClient()?.serviceReload()
|
try await profile.reloadService()
|
||||||
} catch {
|
} catch {
|
||||||
alert = AlertState(error: error)
|
alert = AlertState(error: error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
private func loadSettings() async {
|
private func loadSettings() async {
|
||||||
excludeDefaultRoute = await SharedPreferences.excludeDefaultRoute.get()
|
excludeDefaultRoute = await SharedPreferences.excludeDefaultRoute.get()
|
||||||
autoRouteUseSubRangesByDefault = await SharedPreferences.autoRouteUseSubRangesByDefault.get()
|
autoRouteUseSubRangesByDefault = await SharedPreferences.autoRouteUseSubRangesByDefault.get()
|
||||||
|
|||||||
@@ -1,6 +1,26 @@
|
|||||||
import Library
|
import Library
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
|
#if os(macOS)
|
||||||
|
private struct SettingsNavigationPathKey: EnvironmentKey {
|
||||||
|
static let defaultValue: Binding<NavigationPath>? = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension EnvironmentValues {
|
||||||
|
var settingsNavigationPath: Binding<NavigationPath>? {
|
||||||
|
get { self[SettingsNavigationPathKey.self] }
|
||||||
|
set { self[SettingsNavigationPathKey.self] = newValue }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if os(macOS)
|
||||||
|
public enum SettingsPage: Hashable {
|
||||||
|
case app
|
||||||
|
case core, packetTunnel, onDemandRules, profileOverride, sponsors
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
public struct SettingView: View {
|
public struct SettingView: View {
|
||||||
private enum Tabs: Int, CaseIterable, Identifiable {
|
private enum Tabs: Int, CaseIterable, Identifiable {
|
||||||
var id: Self {
|
var id: Self {
|
||||||
@@ -13,6 +33,25 @@ public struct SettingView: View {
|
|||||||
|
|
||||||
case core, packetTunnel, onDemandRules, profileOverride, sponsors
|
case core, packetTunnel, onDemandRules, profileOverride, sponsors
|
||||||
|
|
||||||
|
#if os(macOS)
|
||||||
|
var page: SettingsPage {
|
||||||
|
switch self {
|
||||||
|
case .app:
|
||||||
|
return .app
|
||||||
|
case .core:
|
||||||
|
return .core
|
||||||
|
case .packetTunnel:
|
||||||
|
return .packetTunnel
|
||||||
|
case .onDemandRules:
|
||||||
|
return .onDemandRules
|
||||||
|
case .profileOverride:
|
||||||
|
return .profileOverride
|
||||||
|
case .sponsors:
|
||||||
|
return .sponsors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
var label: some View {
|
var label: some View {
|
||||||
Label(title, systemImage: iconImage)
|
Label(title, systemImage: iconImage)
|
||||||
}
|
}
|
||||||
@@ -83,16 +122,45 @@ public struct SettingView: View {
|
|||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
var navigationLink: some View {
|
var navigationLink: some View {
|
||||||
FormNavigationLink {
|
#if os(macOS)
|
||||||
contentView
|
FormNavigationLink(value: page) {
|
||||||
} label: {
|
label
|
||||||
label
|
}
|
||||||
}
|
#else
|
||||||
|
FormNavigationLink {
|
||||||
|
contentView
|
||||||
|
} label: {
|
||||||
|
label
|
||||||
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@StateObject private var viewModel = SettingViewModel()
|
#if os(macOS)
|
||||||
|
@MainActor
|
||||||
|
@ViewBuilder
|
||||||
|
private static func destinationView(for page: SettingsPage) -> some View {
|
||||||
|
Group {
|
||||||
|
switch page {
|
||||||
|
case .app:
|
||||||
|
AppView()
|
||||||
|
case .core:
|
||||||
|
CoreView()
|
||||||
|
case .packetTunnel:
|
||||||
|
PacketTunnelView()
|
||||||
|
case .onDemandRules:
|
||||||
|
OnDemandRulesView()
|
||||||
|
case .profileOverride:
|
||||||
|
ProfileOverrideView()
|
||||||
|
case .sponsors:
|
||||||
|
SponsorsView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
@StateObject private var viewModel = SettingViewModel()
|
||||||
public init() {}
|
public init() {}
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
FormView {
|
FormView {
|
||||||
@@ -159,5 +227,10 @@ public struct SettingView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#if os(macOS)
|
||||||
|
.formNavigationDestination(for: SettingsPage.self) { page in
|
||||||
|
Self.destinationView(for: page)
|
||||||
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,6 +93,14 @@ class FileProviderItem: NSObject, NSFileProviderItem {
|
|||||||
return NSNumber(value: contents?.count ?? 0)
|
return NSNumber(value: contents?.count ?? 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Local File Status
|
||||||
|
|
||||||
|
var isUploaded: Bool { true }
|
||||||
|
var isUploading: Bool { false }
|
||||||
|
var isDownloaded: Bool { true }
|
||||||
|
var isDownloading: Bool { false }
|
||||||
|
var isMostRecentVersionDownloaded: Bool { true }
|
||||||
|
|
||||||
private var isDirectory: Bool {
|
private var isDirectory: Bool {
|
||||||
var isDir: ObjCBool = false
|
var isDir: ObjCBool = false
|
||||||
FileManager.default.fileExists(atPath: url.path, isDirectory: &isDir)
|
FileManager.default.fileExists(atPath: url.path, isDirectory: &isDir)
|
||||||
|
|||||||
@@ -0,0 +1,191 @@
|
|||||||
|
import Darwin
|
||||||
|
import Foundation
|
||||||
|
import os
|
||||||
|
|
||||||
|
private let PROC_PIDPATHINFO_MAXSIZE: Int32 = 4096
|
||||||
|
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "ConnectionOwnerLookup")
|
||||||
|
|
||||||
|
enum ConnectionOwnerLookup {
|
||||||
|
struct Result {
|
||||||
|
let userId: Int32
|
||||||
|
let userName: String
|
||||||
|
let processPath: String
|
||||||
|
}
|
||||||
|
|
||||||
|
static func find(
|
||||||
|
ipProtocol: Int32,
|
||||||
|
sourceAddress: String,
|
||||||
|
sourcePort: Int32,
|
||||||
|
destinationAddress: String,
|
||||||
|
destinationPort: Int32
|
||||||
|
) -> Result? {
|
||||||
|
let sourceAddr = parseAddress(sourceAddress)
|
||||||
|
let destAddr = parseAddress(destinationAddress)
|
||||||
|
|
||||||
|
guard let sourceAddr, let destAddr else {
|
||||||
|
logger.error("find: failed to parse addresses")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let pidCount = proc_listpids(UInt32(PROC_ALL_PIDS), 0, nil, 0)
|
||||||
|
guard pidCount > 0 else {
|
||||||
|
logger.error("find: no processes found")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let pidBufferSize = Int(pidCount) * MemoryLayout<pid_t>.size
|
||||||
|
let pids = UnsafeMutablePointer<pid_t>.allocate(capacity: Int(pidCount))
|
||||||
|
defer { pids.deallocate() }
|
||||||
|
|
||||||
|
let actualCount = proc_listpids(UInt32(PROC_ALL_PIDS), 0, pids, Int32(pidBufferSize))
|
||||||
|
guard actualCount > 0 else {
|
||||||
|
logger.error("find: failed to list processes")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let numPids = Int(actualCount) / MemoryLayout<pid_t>.size
|
||||||
|
|
||||||
|
for i in 0 ..< numPids {
|
||||||
|
let pid = pids[i]
|
||||||
|
if pid == 0 { continue }
|
||||||
|
|
||||||
|
if let result = checkProcessForConnection(
|
||||||
|
pid: pid,
|
||||||
|
ipProtocol: ipProtocol,
|
||||||
|
sourceAddr: sourceAddr,
|
||||||
|
sourcePort: UInt16(sourcePort),
|
||||||
|
destAddr: destAddr,
|
||||||
|
destPort: UInt16(destinationPort)
|
||||||
|
) {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func checkProcessForConnection(
|
||||||
|
pid: pid_t,
|
||||||
|
ipProtocol: Int32,
|
||||||
|
sourceAddr: Data,
|
||||||
|
sourcePort: UInt16,
|
||||||
|
destAddr: Data,
|
||||||
|
destPort: UInt16
|
||||||
|
) -> Result? {
|
||||||
|
let bufferSize = proc_pidinfo(pid, PROC_PIDLISTFDS, 0, nil, 0)
|
||||||
|
guard bufferSize > 0 else { return nil }
|
||||||
|
|
||||||
|
let fdBuffer = UnsafeMutableRawPointer.allocate(byteCount: Int(bufferSize), alignment: MemoryLayout<proc_fdinfo>.alignment)
|
||||||
|
defer { fdBuffer.deallocate() }
|
||||||
|
|
||||||
|
let actualSize = proc_pidinfo(pid, PROC_PIDLISTFDS, 0, fdBuffer, bufferSize)
|
||||||
|
guard actualSize > 0 else { return nil }
|
||||||
|
|
||||||
|
let fdCount = Int(actualSize) / MemoryLayout<proc_fdinfo>.size
|
||||||
|
|
||||||
|
for i in 0 ..< fdCount {
|
||||||
|
let fd = fdBuffer.load(fromByteOffset: i * MemoryLayout<proc_fdinfo>.size, as: proc_fdinfo.self)
|
||||||
|
|
||||||
|
guard fd.proc_fdtype == PROX_FDTYPE_SOCKET else { continue }
|
||||||
|
|
||||||
|
var socketInfo = socket_fdinfo()
|
||||||
|
let socketInfoSize = Int32(MemoryLayout<socket_fdinfo>.size)
|
||||||
|
|
||||||
|
let result = proc_pidfdinfo(pid, fd.proc_fd, PROC_PIDFDSOCKETINFO, &socketInfo, socketInfoSize)
|
||||||
|
guard result == socketInfoSize else { continue }
|
||||||
|
|
||||||
|
let soi: in_sockinfo
|
||||||
|
if ipProtocol == IPPROTO_TCP {
|
||||||
|
guard socketInfo.psi.soi_kind == SOCKINFO_TCP else { continue }
|
||||||
|
soi = socketInfo.psi.soi_proto.pri_tcp.tcpsi_ini
|
||||||
|
} else if ipProtocol == IPPROTO_UDP {
|
||||||
|
guard socketInfo.psi.soi_kind == SOCKINFO_IN else { continue }
|
||||||
|
soi = socketInfo.psi.soi_proto.pri_in
|
||||||
|
} else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if matchesConnection(
|
||||||
|
socketInfo: soi,
|
||||||
|
sourceAddr: sourceAddr,
|
||||||
|
sourcePort: sourcePort,
|
||||||
|
destAddr: destAddr,
|
||||||
|
destPort: destPort
|
||||||
|
) {
|
||||||
|
return getProcessInfo(pid: pid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func matchesConnection(
|
||||||
|
socketInfo: in_sockinfo,
|
||||||
|
sourceAddr: Data,
|
||||||
|
sourcePort: UInt16,
|
||||||
|
destAddr: Data,
|
||||||
|
destPort: UInt16
|
||||||
|
) -> Bool {
|
||||||
|
let localPort = UInt16(bigEndian: UInt16(truncatingIfNeeded: socketInfo.insi_lport))
|
||||||
|
let remotePort = UInt16(bigEndian: UInt16(truncatingIfNeeded: socketInfo.insi_fport))
|
||||||
|
|
||||||
|
guard localPort == sourcePort, remotePort == destPort else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var localAddr = socketInfo.insi_laddr
|
||||||
|
var remoteAddr = socketInfo.insi_faddr
|
||||||
|
|
||||||
|
let localData: Data
|
||||||
|
let remoteData: Data
|
||||||
|
|
||||||
|
if sourceAddr.count == 4 {
|
||||||
|
localData = Data(bytes: &localAddr.ina_46.i46a_addr4, count: 4)
|
||||||
|
remoteData = Data(bytes: &remoteAddr.ina_46.i46a_addr4, count: 4)
|
||||||
|
} else {
|
||||||
|
localData = Data(bytes: &localAddr.ina_6, count: 16)
|
||||||
|
remoteData = Data(bytes: &remoteAddr.ina_6, count: 16)
|
||||||
|
}
|
||||||
|
|
||||||
|
return localData == sourceAddr && remoteData == destAddr
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func getProcessInfo(pid: pid_t) -> Result? {
|
||||||
|
let pathBuffer = UnsafeMutablePointer<CChar>.allocate(capacity: Int(PROC_PIDPATHINFO_MAXSIZE))
|
||||||
|
defer { pathBuffer.deallocate() }
|
||||||
|
|
||||||
|
let pathLength = proc_pidpath(pid, pathBuffer, UInt32(PROC_PIDPATHINFO_MAXSIZE))
|
||||||
|
let processPath = pathLength > 0 ? String(cString: pathBuffer) : ""
|
||||||
|
|
||||||
|
var info = proc_bsdinfo()
|
||||||
|
let infoSize = Int32(MemoryLayout<proc_bsdinfo>.size)
|
||||||
|
let result = proc_pidinfo(pid, PROC_PIDTBSDINFO, 0, &info, infoSize)
|
||||||
|
|
||||||
|
guard result == infoSize else { return nil }
|
||||||
|
|
||||||
|
let uid = Int32(info.pbi_uid)
|
||||||
|
let userName: String
|
||||||
|
|
||||||
|
if let pw = getpwuid(info.pbi_uid) {
|
||||||
|
userName = String(cString: pw.pointee.pw_name)
|
||||||
|
} else {
|
||||||
|
userName = String(uid)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result(userId: uid, userName: userName, processPath: processPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func parseAddress(_ address: String) -> Data? {
|
||||||
|
var addr4 = in_addr()
|
||||||
|
if inet_pton(AF_INET, address, &addr4) == 1 {
|
||||||
|
return Data(bytes: &addr4, count: 4)
|
||||||
|
}
|
||||||
|
|
||||||
|
var addr6 = in6_addr()
|
||||||
|
if inet_pton(AF_INET6, address, &addr6) == 1 {
|
||||||
|
return Data(bytes: &addr6, count: 16)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>$(PRODUCT_NAME)</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>$(MARKETING_VERSION)</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||||
|
<key>LSMinimumSystemVersion</key>
|
||||||
|
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
|
||||||
|
<key>BasePackageIdentifier</key>
|
||||||
|
<string>$(BASE_PACKAGE_IDENTIFIER)</string>
|
||||||
|
<key>AppGroupIdentifier</key>
|
||||||
|
<string>$(APP_GROUP_IDENTIFIER)</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>Label</key>
|
||||||
|
<string>io.nekohasekai.sfavt.helper</string>
|
||||||
|
<key>BundleProgram</key>
|
||||||
|
<string>Contents/Helpers/RootHelper</string>
|
||||||
|
<key>MachServices</key>
|
||||||
|
<dict>
|
||||||
|
<key>287TTNZF8L.io.nekohasekai.sfavt.helper</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
<key>AssociatedBundleIdentifiers</key>
|
||||||
|
<array>
|
||||||
|
<string>io.nekohasekai.sfavt.standalone</string>
|
||||||
|
</array>
|
||||||
|
<key>RunAtLoad</key>
|
||||||
|
<true/>
|
||||||
|
<key>KeepAlive</key>
|
||||||
|
<dict>
|
||||||
|
<key>SuccessfulExit</key>
|
||||||
|
<false/>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>com.apple.security.app-sandbox</key>
|
||||||
|
<false/>
|
||||||
|
<key>com.apple.security.application-groups</key>
|
||||||
|
<array>
|
||||||
|
<string>$(TeamIdentifierPrefix)$(BASE_PACKAGE_IDENTIFIER)</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
import Foundation
|
||||||
|
import Library
|
||||||
|
import os
|
||||||
|
|
||||||
|
private let logger = Logger(category: "RootHelper")
|
||||||
|
|
||||||
|
class RootHelperService: NSObject {
|
||||||
|
private var listener: NSXPCListener?
|
||||||
|
|
||||||
|
func start() {
|
||||||
|
setupLogging()
|
||||||
|
startXPCListener()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupLogging() {
|
||||||
|
let basePath = "/var/log/sing-box"
|
||||||
|
try? FileManager.default.createDirectory(atPath: basePath, withIntermediateDirectories: true)
|
||||||
|
|
||||||
|
let logPath = basePath + "/roothelper.log"
|
||||||
|
freopen(logPath, "a", stderr)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func startXPCListener() {
|
||||||
|
let machServiceName = getMachServiceName()
|
||||||
|
listener = NSXPCListener(machServiceName: machServiceName)
|
||||||
|
listener?.delegate = self
|
||||||
|
listener?.resume()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func getMachServiceName() -> String {
|
||||||
|
if let identifier = Bundle.main.object(forInfoDictionaryKey: "AppGroupIdentifier") as? String {
|
||||||
|
return "\(identifier).helper"
|
||||||
|
}
|
||||||
|
fatalError("Missing AppGroupIdentifier in Info.plist")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension RootHelperService: NSXPCListenerDelegate {
|
||||||
|
func listener(_: NSXPCListener, shouldAcceptNewConnection newConnection: NSXPCConnection) -> Bool {
|
||||||
|
let allowedBundleIDs = [
|
||||||
|
AppConfiguration.systemExtensionBundleID,
|
||||||
|
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: RootHelperProtocol.self)
|
||||||
|
RootHelperXPC.configureInterface(exportedInterface)
|
||||||
|
newConnection.exportedInterface = exportedInterface
|
||||||
|
newConnection.exportedObject = self
|
||||||
|
newConnection.resume()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension RootHelperService: RootHelperProtocol {
|
||||||
|
func findConnectionOwner(
|
||||||
|
ipProtocol: Int32,
|
||||||
|
sourceAddress: String,
|
||||||
|
sourcePort: Int32,
|
||||||
|
destinationAddress: String,
|
||||||
|
destinationPort: Int32,
|
||||||
|
reply: @escaping (ConnectionOwnerResult?, NSError?) -> Void
|
||||||
|
) {
|
||||||
|
guard let result = ConnectionOwnerLookup.find(
|
||||||
|
ipProtocol: ipProtocol,
|
||||||
|
sourceAddress: sourceAddress,
|
||||||
|
sourcePort: sourcePort,
|
||||||
|
destinationAddress: destinationAddress,
|
||||||
|
destinationPort: destinationPort
|
||||||
|
) else {
|
||||||
|
let error = NSError(domain: "RootHelper", code: -1, userInfo: [
|
||||||
|
NSLocalizedDescriptionKey: "Connection owner not found",
|
||||||
|
])
|
||||||
|
logger.error("findConnectionOwner: \(error.localizedDescription)")
|
||||||
|
reply(nil, error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let ownerResult = ConnectionOwnerResult(
|
||||||
|
userId: result.userId,
|
||||||
|
userName: result.userName,
|
||||||
|
processPath: result.processPath
|
||||||
|
)
|
||||||
|
reply(ownerResult, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getWorkingDirectorySize(reply: @escaping (Int64, NSError?) -> Void) {
|
||||||
|
let size = WorkingDirectoryManager.getSize()
|
||||||
|
reply(size, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func cleanWorkingDirectory(reply: @escaping (NSError?) -> Void) {
|
||||||
|
do {
|
||||||
|
try WorkingDirectoryManager.clean()
|
||||||
|
reply(nil)
|
||||||
|
} catch {
|
||||||
|
logger.error("cleanWorkingDirectory error: \(error.localizedDescription)")
|
||||||
|
reply(error as NSError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import Foundation
|
||||||
|
import Library
|
||||||
|
|
||||||
|
enum WorkingDirectoryManager {
|
||||||
|
private static var workingDirectoryPath: String {
|
||||||
|
"/var/root/Library/Containers/\(AppConfiguration.systemExtensionBundleID)/Data/Working"
|
||||||
|
}
|
||||||
|
|
||||||
|
static func getSize() -> Int64 {
|
||||||
|
let path = workingDirectoryPath
|
||||||
|
guard FileManager.default.fileExists(atPath: path) else {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
var totalSize: Int64 = 0
|
||||||
|
let enumerator = FileManager.default.enumerator(atPath: path)
|
||||||
|
|
||||||
|
while let file = enumerator?.nextObject() as? String {
|
||||||
|
let filePath = (path as NSString).appendingPathComponent(file)
|
||||||
|
if let attrs = try? FileManager.default.attributesOfItem(atPath: filePath),
|
||||||
|
let size = attrs[.size] as? Int64
|
||||||
|
{
|
||||||
|
totalSize += size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return totalSize
|
||||||
|
}
|
||||||
|
|
||||||
|
static func clean() throws {
|
||||||
|
let path = workingDirectoryPath
|
||||||
|
guard FileManager.default.fileExists(atPath: path) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let contents = try FileManager.default.contentsOfDirectory(atPath: path)
|
||||||
|
for item in contents {
|
||||||
|
let itemPath = (path as NSString).appendingPathComponent(item)
|
||||||
|
try FileManager.default.removeItem(atPath: itemPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
let service = RootHelperService()
|
||||||
|
service.start()
|
||||||
|
dispatchMain()
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import AppIntents
|
import AppIntents
|
||||||
import Foundation
|
import Foundation
|
||||||
import Libbox
|
|
||||||
import Library
|
import Library
|
||||||
|
|
||||||
struct StartServiceIntent: AppIntent {
|
struct StartServiceIntent: AppIntent {
|
||||||
@@ -18,7 +17,7 @@ struct StartServiceIntent: AppIntent {
|
|||||||
|
|
||||||
func perform() async throws -> some IntentResult & ProvidesDialog {
|
func perform() async throws -> some IntentResult & ProvidesDialog {
|
||||||
guard let extensionProfile = try await (ExtensionProfile.load()) else {
|
guard let extensionProfile = try await (ExtensionProfile.load()) else {
|
||||||
throw NSError(domain: "NetworkExtension not installed", code: 0)
|
throw NSError(domain: "IntentsExtension", code: 0, userInfo: [NSLocalizedDescriptionKey: String(localized: "NetworkExtension not installed")])
|
||||||
}
|
}
|
||||||
let profileList = try await ProfileManager.list()
|
let profileList = try await ProfileManager.list()
|
||||||
let specifiedProfile = profileList.first { $0.name == profile }
|
let specifiedProfile = profileList.first { $0.name == profile }
|
||||||
@@ -30,13 +29,13 @@ struct StartServiceIntent: AppIntent {
|
|||||||
profileChanged = true
|
profileChanged = true
|
||||||
}
|
}
|
||||||
} else if profile != "default" {
|
} else if profile != "default" {
|
||||||
throw NSError(domain: "Specified profile not found: \(profile)", code: 0)
|
throw NSError(domain: "IntentsExtension", code: 0, userInfo: [NSLocalizedDescriptionKey: String(localized: "Specified profile not found: \(profile)")])
|
||||||
}
|
}
|
||||||
if await extensionProfile.status == .connected {
|
if await extensionProfile.status == .connected {
|
||||||
if !profileChanged {
|
if !profileChanged {
|
||||||
return .result(dialog: "Service is already running")
|
return .result(dialog: "Service is already running")
|
||||||
}
|
}
|
||||||
try LibboxNewStandaloneCommandClient()!.serviceReload()
|
try await extensionProfile.reloadService()
|
||||||
} else if await extensionProfile.status.isConnected {
|
} else if await extensionProfile.status.isConnected {
|
||||||
try await extensionProfile.restart()
|
try await extensionProfile.restart()
|
||||||
} else {
|
} else {
|
||||||
@@ -61,7 +60,7 @@ struct RestartServiceIntent: AppIntent {
|
|||||||
return .result(dialog: "Service is not installed")
|
return .result(dialog: "Service is not installed")
|
||||||
}
|
}
|
||||||
if await extensionProfile.status == .connected {
|
if await extensionProfile.status == .connected {
|
||||||
try LibboxNewStandaloneCommandClient()!.serviceReload()
|
try await extensionProfile.reloadService()
|
||||||
} else if await extensionProfile.status.isConnected {
|
} else if await extensionProfile.status.isConnected {
|
||||||
try await extensionProfile.restart()
|
try await extensionProfile.restart()
|
||||||
} else {
|
} else {
|
||||||
@@ -145,7 +144,7 @@ struct GetCurrentProfile: AppIntent {
|
|||||||
|
|
||||||
func perform() async throws -> some IntentResult & ReturnsValue<String> {
|
func perform() async throws -> some IntentResult & ReturnsValue<String> {
|
||||||
guard let profile = try await ProfileManager.get(SharedPreferences.selectedProfileID.get()) else {
|
guard let profile = try await ProfileManager.get(SharedPreferences.selectedProfileID.get()) else {
|
||||||
throw NSError(domain: "No profile selected", code: 0)
|
throw NSError(domain: "IntentsExtension", code: 0, userInfo: [NSLocalizedDescriptionKey: String(localized: "No profile selected")])
|
||||||
}
|
}
|
||||||
return .result(value: profile.name)
|
return .result(value: profile.name)
|
||||||
}
|
}
|
||||||
@@ -167,10 +166,10 @@ struct UpdateProfileIntent: AppIntent {
|
|||||||
init() {}
|
init() {}
|
||||||
func perform() async throws -> some IntentResult & ProvidesDialog {
|
func perform() async throws -> some IntentResult & ProvidesDialog {
|
||||||
guard let profile = try await ProfileManager.get(by: profile) else {
|
guard let profile = try await ProfileManager.get(by: profile) else {
|
||||||
throw NSError(domain: "Specified profile not found: \(profile)", code: 0)
|
throw NSError(domain: "IntentsExtension", code: 0, userInfo: [NSLocalizedDescriptionKey: String(localized: "Specified profile not found: \(profile)")])
|
||||||
}
|
}
|
||||||
if profile.type != .remote {
|
if profile.type != .remote {
|
||||||
throw NSError(domain: "Specified profile is not a remote profile", code: 0)
|
throw NSError(domain: "IntentsExtension", code: 0, userInfo: [NSLocalizedDescriptionKey: String(localized: "Specified profile is not a remote profile")])
|
||||||
}
|
}
|
||||||
try await profile.updateRemoteProfile()
|
try await profile.updateRemoteProfile()
|
||||||
return .result(dialog: "Profile updated")
|
return .result(dialog: "Profile updated")
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ public extension Date {
|
|||||||
var relativeFormat: String {
|
var relativeFormat: String {
|
||||||
let formatter = RelativeDateTimeFormatter()
|
let formatter = RelativeDateTimeFormatter()
|
||||||
formatter.unitsStyle = .full
|
formatter.unitsStyle = .full
|
||||||
|
formatter.dateTimeStyle = .named
|
||||||
return formatter.localizedString(for: self, relativeTo: Date())
|
return formatter.localizedString(for: self, relativeTo: Date())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -119,61 +119,62 @@ public extension UTType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#if !os(tvOS)
|
#if !os(tvOS)
|
||||||
// MARK: - FileDocument for Export
|
|
||||||
|
|
||||||
public struct ProfileExportDocument: FileDocument {
|
// MARK: - FileDocument for Export
|
||||||
public static var readableContentTypes: [UTType] { [.profile] }
|
|
||||||
|
|
||||||
public let data: Data
|
public struct ProfileExportDocument: FileDocument {
|
||||||
public let filename: String
|
public static var readableContentTypes: [UTType] { [.profile] }
|
||||||
|
|
||||||
public init(content: LibboxProfileContent) throws {
|
public let data: Data
|
||||||
guard let encoded = content.encode() else {
|
public let filename: String
|
||||||
throw NSError(domain: "ProfileExportDocument", code: -1, userInfo: [NSLocalizedDescriptionKey: "Failed to encode profile"])
|
|
||||||
|
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 {
|
public init(configuration: ReadConfiguration) throws {
|
||||||
guard let data = configuration.file.regularFileContents else {
|
guard let data = configuration.file.regularFileContents else {
|
||||||
throw CocoaError(.fileReadCorruptFile)
|
throw CocoaError(.fileReadCorruptFile)
|
||||||
|
}
|
||||||
|
self.data = data
|
||||||
|
filename = "profile.bpf"
|
||||||
}
|
}
|
||||||
self.data = data
|
|
||||||
filename = "profile.bpf"
|
|
||||||
}
|
|
||||||
|
|
||||||
public func fileWrapper(configuration _: WriteConfiguration) throws -> FileWrapper {
|
public func fileWrapper(configuration _: WriteConfiguration) throws -> FileWrapper {
|
||||||
FileWrapper(regularFileWithContents: data)
|
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)
|
|
||||||
}
|
}
|
||||||
self.content = content
|
|
||||||
filename = "profile.json"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public func fileWrapper(configuration _: WriteConfiguration) throws -> FileWrapper {
|
public struct ProfileJSONExportDocument: FileDocument {
|
||||||
guard let data = content.data(using: .utf8) else {
|
public static var readableContentTypes: [UTType] { [.json] }
|
||||||
throw CocoaError(.fileWriteInapplicableStringEncoding)
|
|
||||||
|
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
|
#endif
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ public extension Profile {
|
|||||||
if await SharedPreferences.selectedProfileID.get() == id {
|
if await SharedPreferences.selectedProfileID.get() == id {
|
||||||
if let profile = try? await ExtensionProfile.load() {
|
if let profile = try? await ExtensionProfile.load() {
|
||||||
if await profile.status == .connected {
|
if await profile.status == .connected {
|
||||||
try LibboxNewStandaloneCommandClient()!.serviceReload()
|
try await profile.reloadService()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import BinaryCodable
|
import BinaryCodable
|
||||||
import Foundation
|
import Foundation
|
||||||
import GRDB
|
import GRDB
|
||||||
|
import os
|
||||||
|
|
||||||
|
private let logger = Logger(category: "SharedPreferences")
|
||||||
|
|
||||||
extension SharedPreferences {
|
extension SharedPreferences {
|
||||||
public class Preference<T: Codable> {
|
public class Preference<T: Codable> {
|
||||||
@@ -16,7 +19,7 @@ extension SharedPreferences {
|
|||||||
do {
|
do {
|
||||||
return try await SharedPreferences.read(name) ?? defaultValue
|
return try await SharedPreferences.read(name) ?? defaultValue
|
||||||
} catch {
|
} catch {
|
||||||
NSLog("read preferences error: \(error)")
|
logger.error("read preferences error: \(error)")
|
||||||
return defaultValue
|
return defaultValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -31,7 +34,7 @@ extension SharedPreferences {
|
|||||||
do {
|
do {
|
||||||
try await SharedPreferences.write(name, newValue)
|
try await SharedPreferences.write(name, newValue)
|
||||||
} catch {
|
} catch {
|
||||||
NSLog("write preferences error: \(error)")
|
logger.error("write preferences error: \(error)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import Libbox
|
import Libbox
|
||||||
|
import os
|
||||||
|
|
||||||
|
private let logger = Logger(category: "CommandClient")
|
||||||
|
|
||||||
public struct LogEntry: Identifiable {
|
public struct LogEntry: Identifiable {
|
||||||
public let id = UUID()
|
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() {
|
public func filterConnectionsNow() {
|
||||||
guard let message = rawConnections else {
|
guard let message = rawConnections else {
|
||||||
return
|
return
|
||||||
@@ -222,7 +232,7 @@ public class CommandClient: ObservableObject {
|
|||||||
commandClient.isConnected = false
|
commandClient.isConnected = false
|
||||||
}
|
}
|
||||||
if let message {
|
if let message {
|
||||||
NSLog("client disconnected: \(message)")
|
logger.debug("client disconnected: \(message)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -234,7 +244,7 @@ public class CommandClient: ObservableObject {
|
|||||||
|
|
||||||
func clearLogs() {
|
func clearLogs() {
|
||||||
DispatchQueue.main.async { [self] in
|
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 Foundation
|
||||||
import SwiftUI
|
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
|
@MainActor
|
||||||
public class ExtensionEnvironments: ObservableObject {
|
public class ExtensionEnvironments: ObservableObject {
|
||||||
@Published public var commandClient = CommandClient([.log, .status, .groups, .clashMode, .connections])
|
@Published public var commandClient = CommandClient([.log, .status, .groups, .clashMode, .connections])
|
||||||
@Published public var extensionProfileLoading = true
|
@Published public var extensionProfileLoading = true
|
||||||
@Published public var extensionProfile: ExtensionProfile?
|
@Published public var extensionProfile: ExtensionProfile?
|
||||||
@Published public var emptyProfiles = false
|
@Published public var emptyProfiles = false
|
||||||
|
@Published public var pendingImportRemoteProfile: ImportRemoteProfileRequest?
|
||||||
|
|
||||||
public let profileUpdate = ObjectWillChangePublisher()
|
public let profileUpdate = ObjectWillChangePublisher()
|
||||||
public let selectedProfileUpdate = ObjectWillChangePublisher()
|
public let selectedProfileUpdate = ObjectWillChangePublisher()
|
||||||
@@ -14,12 +143,6 @@ public class ExtensionEnvironments: ObservableObject {
|
|||||||
|
|
||||||
public init() {}
|
public init() {}
|
||||||
|
|
||||||
nonisolated deinit {
|
|
||||||
Task { @MainActor in
|
|
||||||
commandClient.disconnect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public func postReload() {
|
public func postReload() {
|
||||||
Task {
|
Task {
|
||||||
await reload()
|
await reload()
|
||||||
|
|||||||
@@ -22,14 +22,17 @@ public class ExtensionPlatformInterface: NSObject, LibboxPlatformInterfaceProtoc
|
|||||||
|
|
||||||
private func openTun0(_ options: LibboxTunOptionsProtocol?, _ ret0_: UnsafeMutablePointer<Int32>?) async throws {
|
private func openTun0(_ options: LibboxTunOptionsProtocol?, _ ret0_: UnsafeMutablePointer<Int32>?) async throws {
|
||||||
guard let options else {
|
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 {
|
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 prefs = tunnel.overridePreferences ?? ExtensionProvider.OverridePreferences()
|
||||||
let excludeAPNs = await SharedPreferences.excludeAPNsRoute.get()
|
let autoRouteUseSubRangesByDefault = prefs.autoRouteUseSubRangesByDefault
|
||||||
|
let excludeAPNs = prefs.excludeAPNsRoute
|
||||||
|
let excludeDefaultRoute = prefs.excludeDefaultRoute
|
||||||
|
let systemProxyEnabled = prefs.systemProxyEnabled
|
||||||
|
|
||||||
let settings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: "127.0.0.1")
|
let settings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: "127.0.0.1")
|
||||||
if options.getAutoRoute() {
|
if options.getAutoRoute() {
|
||||||
@@ -78,7 +81,7 @@ public class ExtensionPlatformInterface: NSObject, LibboxPlatformInterfaceProtoc
|
|||||||
let ipv4RoutePrefix = inet4RouteExcludeAddressIterator.next()!
|
let ipv4RoutePrefix = inet4RouteExcludeAddressIterator.next()!
|
||||||
ipv4ExcludeRoutes.append(NEIPv4Route(destinationAddress: ipv4RoutePrefix.address(), subnetMask: ipv4RoutePrefix.mask()))
|
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
|
if !ipv4ExcludeRoutes.contains(where: { it in
|
||||||
it.destinationAddress == "0.0.0.0" && it.destinationSubnetMask == "255.255.255.254"
|
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())))
|
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
|
if !ipv6ExcludeRoutes.contains(where: { it in
|
||||||
it.destinationAddress == "::" && it.destinationNetworkPrefixLength == 127
|
it.destinationAddress == "::" && it.destinationNetworkPrefixLength == 127
|
||||||
}) {
|
}) {
|
||||||
@@ -152,7 +155,7 @@ public class ExtensionPlatformInterface: NSObject, LibboxPlatformInterfaceProtoc
|
|||||||
let proxyServer = NEProxyServer(address: options.getHTTPProxyServer(), port: Int(options.getHTTPProxyServerPort()))
|
let proxyServer = NEProxyServer(address: options.getHTTPProxyServer(), port: Int(options.getHTTPProxyServerPort()))
|
||||||
proxySettings.httpServer = proxyServer
|
proxySettings.httpServer = proxyServer
|
||||||
proxySettings.httpsServer = proxyServer
|
proxySettings.httpsServer = proxyServer
|
||||||
if await SharedPreferences.systemProxyEnabled.get() {
|
if systemProxyEnabled {
|
||||||
proxySettings.httpEnabled = true
|
proxySettings.httpEnabled = true
|
||||||
proxySettings.httpsEnabled = true
|
proxySettings.httpsEnabled = true
|
||||||
}
|
}
|
||||||
@@ -194,7 +197,7 @@ public class ExtensionPlatformInterface: NSObject, LibboxPlatformInterfaceProtoc
|
|||||||
if tunFdFromLoop != -1 {
|
if tunFdFromLoop != -1 {
|
||||||
ret0_.pointee = tunFdFromLoop
|
ret0_.pointee = tunFdFromLoop
|
||||||
} else {
|
} 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 autoDetectControl(_: Int32) throws {}
|
||||||
|
|
||||||
public func findConnectionOwner(_: Int32, sourceAddress _: String?, sourcePort _: Int32, destinationAddress _: String?, destinationPort _: Int32, ret0_ _: UnsafeMutablePointer<Int32>?) throws {
|
public func findConnectionOwner(_ ipProtocol: Int32, sourceAddress: String?, sourcePort: Int32, destinationAddress: String?, destinationPort: Int32) throws -> LibboxConnectionOwner {
|
||||||
throw NSError(domain: "not implemented", code: 0)
|
#if os(macOS)
|
||||||
}
|
if Variant.useSystemExtension {
|
||||||
|
guard let sourceAddress, let destinationAddress else {
|
||||||
public func packageName(byUid _: Int32, error _: NSErrorPointer) -> String {
|
throw NSError(domain: "findConnectionOwner", code: 0, userInfo: [
|
||||||
""
|
NSLocalizedDescriptionKey: "Missing source or destination address",
|
||||||
}
|
])
|
||||||
|
}
|
||||||
public func uid(byPackageName _: String?, ret0_ _: UnsafeMutablePointer<Int32>?) throws {
|
let owner = try RootHelperClient.shared.findConnectionOwner(
|
||||||
throw NSError(domain: "not implemented", code: 0)
|
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 {
|
public func useProcFS() -> Bool {
|
||||||
@@ -264,7 +280,7 @@ public class ExtensionPlatformInterface: NSObject, LibboxPlatformInterfaceProtoc
|
|||||||
|
|
||||||
public func getInterfaces() throws -> LibboxNetworkInterfaceIteratorProtocol {
|
public func getInterfaces() throws -> LibboxNetworkInterfaceIteratorProtocol {
|
||||||
guard let nwMonitor else {
|
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
|
let path = nwMonitor.currentPath
|
||||||
if path.status == .unsatisfied {
|
if path.status == .unsatisfied {
|
||||||
@@ -313,10 +329,10 @@ public class ExtensionPlatformInterface: NSObject, LibboxPlatformInterfaceProtoc
|
|||||||
}
|
}
|
||||||
|
|
||||||
public func includeAllNetworks() -> Bool {
|
public func includeAllNetworks() -> Bool {
|
||||||
#if !os(tvOS)
|
#if os(tvOS)
|
||||||
return SharedPreferences.includeAllNetworks.getBlocking()
|
|
||||||
#else
|
|
||||||
return false
|
return false
|
||||||
|
#else
|
||||||
|
return tunnel.overridePreferences?.includeAllNetworks ?? false
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -324,12 +340,20 @@ public class ExtensionPlatformInterface: NSObject, LibboxPlatformInterfaceProtoc
|
|||||||
guard let networkSettings else {
|
guard let networkSettings else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
tunnel.reasserting = true
|
runBlocking {
|
||||||
tunnel.setTunnelNetworkSettings(nil) { _ in
|
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? {
|
public func readWIFIState() -> LibboxWIFIState? {
|
||||||
@@ -342,6 +366,9 @@ public class ExtensionPlatformInterface: NSObject, LibboxPlatformInterfaceProtoc
|
|||||||
}
|
}
|
||||||
return LibboxWIFIState(network.ssid, wifiBSSID: network.bssid)!
|
return LibboxWIFIState(network.ssid, wifiBSSID: network.bssid)!
|
||||||
#elseif os(macOS)
|
#elseif os(macOS)
|
||||||
|
if Variant.useSystemExtension {
|
||||||
|
return UserServiceClient.shared.readWIFIState()
|
||||||
|
}
|
||||||
guard let interface = CWWiFiClient.shared().interface() else {
|
guard let interface = CWWiFiClient.shared().interface() else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -425,6 +452,8 @@ public class ExtensionPlatformInterface: NSObject, LibboxPlatformInterfaceProtoc
|
|||||||
|
|
||||||
func reset() {
|
func reset() {
|
||||||
networkSettings = nil
|
networkSettings = nil
|
||||||
|
nwMonitor?.cancel()
|
||||||
|
nwMonitor = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
public func send(_ notification: LibboxNotification?) throws {
|
public func send(_ notification: LibboxNotification?) throws {
|
||||||
@@ -432,6 +461,12 @@ public class ExtensionPlatformInterface: NSObject, LibboxPlatformInterfaceProtoc
|
|||||||
guard let notification else {
|
guard let notification else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
#if os(macOS)
|
||||||
|
if Variant.useSystemExtension {
|
||||||
|
try UserServiceClient.shared.sendNotification(notification)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
#endif
|
||||||
let center = UNUserNotificationCenter.current()
|
let center = UNUserNotificationCenter.current()
|
||||||
let content = UNMutableNotificationContent()
|
let content = UNMutableNotificationContent()
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import Libbox
|
import Libbox
|
||||||
import NetworkExtension
|
import NetworkExtension
|
||||||
|
import os
|
||||||
|
|
||||||
|
private let logger = Logger(category: "ExtensionProfile")
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
public class ExtensionProfile: ObservableObject {
|
public class ExtensionProfile: ObservableObject {
|
||||||
@@ -24,17 +27,19 @@ public class ExtensionProfile: ObservableObject {
|
|||||||
observer = NotificationCenter.default.addObserver(
|
observer = NotificationCenter.default.addObserver(
|
||||||
forName: NSNotification.Name.NEVPNStatusDidChange,
|
forName: NSNotification.Name.NEVPNStatusDidChange,
|
||||||
object: manager.connection,
|
object: manager.connection,
|
||||||
queue: .main
|
queue: nil
|
||||||
) { [weak self] notification in
|
) { [weak self] notification in
|
||||||
guard let self else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
guard let connection = notification.object as? NEVPNConnection else {
|
guard let connection = notification.object as? NEVPNConnection else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
self.connection = connection
|
Task { @MainActor in
|
||||||
self.status = connection.status
|
guard let self else {
|
||||||
self.connectedDate = connection.connectedDate
|
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 {
|
public func start() async throws {
|
||||||
await fetchProfile()
|
try await fetchProfile()
|
||||||
manager.isEnabled = true
|
manager.isEnabled = true
|
||||||
let alwaysOn = await SharedPreferences.alwaysOn.get()
|
let alwaysOn = await SharedPreferences.alwaysOn.get()
|
||||||
let onDemandEnabled = await SharedPreferences.onDemandEnabled.get()
|
let onDemandEnabled = await SharedPreferences.onDemandEnabled.get()
|
||||||
@@ -96,29 +101,73 @@ public class ExtensionProfile: ObservableObject {
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
try await manager.saveToPreferences()
|
try await manager.saveToPreferences()
|
||||||
#if os(macOS)
|
let options = try await prepareStartOptions()
|
||||||
if Variant.useSystemExtension {
|
try manager.connection.startVPNTunnel(options: options)
|
||||||
try manager.connection.startVPNTunnel(options: [
|
|
||||||
"username": NSString(string: NSUserName()),
|
|
||||||
"manualStart": NSNumber(value: true),
|
|
||||||
])
|
|
||||||
return
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
try manager.connection.startVPNTunnel(options: [
|
|
||||||
"manualStart": NSNumber(value: true),
|
|
||||||
])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public func fetchProfile() async {
|
public func reloadService() async throws {
|
||||||
do {
|
let options = try await prepareStartOptions()
|
||||||
if let profile = try await ProfileManager.get(Int64(SharedPreferences.selectedProfileID.get())) {
|
let data = try ExtensionStartOptions.encode(options)
|
||||||
if profile.type == .icloud {
|
guard let session = connection as? NETunnelProviderSession else {
|
||||||
_ = try profile.read()
|
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 {
|
do {
|
||||||
try LibboxNewStandaloneCommandClient()!.serviceClose()
|
try LibboxNewStandaloneCommandClient()!.serviceClose()
|
||||||
} catch {
|
} catch {
|
||||||
NSLog("serviceClose error: \(error.localizedDescription)")
|
logger.debug("serviceClose error: \(error.localizedDescription)")
|
||||||
}
|
}
|
||||||
manager.connection.stopVPNTunnel()
|
manager.connection.stopVPNTunnel()
|
||||||
}
|
}
|
||||||
@@ -142,7 +191,7 @@ public class ExtensionProfile: ObservableObject {
|
|||||||
try await Task.sleep(nanoseconds: NSEC_PER_SEC)
|
try await Task.sleep(nanoseconds: NSEC_PER_SEC)
|
||||||
waitSeconds += 1
|
waitSeconds += 1
|
||||||
if waitSeconds >= 5 {
|
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()
|
try await start()
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import Libbox
|
import Libbox
|
||||||
import NetworkExtension
|
import NetworkExtension
|
||||||
|
import os.log
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
import WidgetKit
|
import WidgetKit
|
||||||
#endif
|
#endif
|
||||||
@@ -9,20 +10,108 @@ import NetworkExtension
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
open class ExtensionProvider: NEPacketTunnelProvider {
|
open class ExtensionProvider: NEPacketTunnelProvider {
|
||||||
public var username: String?
|
private static let logger = Logger(category: "ExtensionProvider")
|
||||||
private var commandServer: LibboxCommandServer!
|
|
||||||
|
public private(set) var commandServer: LibboxCommandServer?
|
||||||
private var platformInterface: ExtensionPlatformInterface!
|
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()
|
let options = LibboxSetupOptions()
|
||||||
options.basePath = FilePath.sharedDirectory.relativePath
|
options.basePath = basePath
|
||||||
options.workingPath = FilePath.workingDirectory.relativePath
|
options.workingPath = workingPath
|
||||||
options.tempPath = FilePath.cacheDirectory.relativePath
|
options.tempPath = tempPath
|
||||||
|
|
||||||
options.logMaxLines = 3000
|
options.logMaxLines = 3000
|
||||||
|
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
options.commandServerListenPort = await SharedPreferences.commandServerPort.get()
|
if let port = effectiveOptions?["commandServerPort"] as? NSNumber {
|
||||||
options.commandServerSecret = await SharedPreferences.commandServerSecret.get()
|
options.commandServerListenPort = port.int32Value
|
||||||
|
}
|
||||||
|
if let secret = effectiveOptions?["commandServerSecret"] as? String {
|
||||||
|
options.commandServerSecret = secret
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
var setupError: NSError?
|
var setupError: NSError?
|
||||||
@@ -31,13 +120,8 @@ open class ExtensionProvider: NEPacketTunnelProvider {
|
|||||||
throw ExtensionStartupError("(packet-tunnel) error: setup service: \(setupError.localizedDescription)")
|
throw ExtensionStartupError("(packet-tunnel) error: setup service: \(setupError.localizedDescription)")
|
||||||
}
|
}
|
||||||
|
|
||||||
var stderrError: NSError?
|
let ignoreMemoryLimit = (effectiveOptions?["ignoreMemoryLimit"] as? NSNumber)?.boolValue ?? false
|
||||||
LibboxRedirectStderr(FilePath.cacheDirectory.appendingPathComponent("stderr.log").relativePath, &stderrError)
|
LibboxSetMemoryLimit(!ignoreMemoryLimit)
|
||||||
if let stderrError {
|
|
||||||
throw ExtensionStartupError("(packet-tunnel) redirect stderr error: \(stderrError.localizedDescription)")
|
|
||||||
}
|
|
||||||
|
|
||||||
await LibboxSetMemoryLimit(!SharedPreferences.ignoreMemoryLimit.get())
|
|
||||||
|
|
||||||
if platformInterface == nil {
|
if platformInterface == nil {
|
||||||
platformInterface = ExtensionPlatformInterface(self)
|
platformInterface = ExtensionPlatformInterface(self)
|
||||||
@@ -48,12 +132,31 @@ open class ExtensionProvider: NEPacketTunnelProvider {
|
|||||||
throw ExtensionStartupError("(packet-tunnel): create command server error: \(error.localizedDescription)")
|
throw ExtensionStartupError("(packet-tunnel): create command server error: \(error.localizedDescription)")
|
||||||
}
|
}
|
||||||
do {
|
do {
|
||||||
try commandServer.start()
|
try commandServer!.start()
|
||||||
} catch {
|
} catch {
|
||||||
throw ExtensionStartupError("(packet-tunnel): start command server error: \(error.localizedDescription)")
|
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")
|
writeMessage("(packet-tunnel): Here I stand")
|
||||||
try await startService()
|
try await startService()
|
||||||
|
#if os(macOS)
|
||||||
|
if Variant.useSystemExtension {
|
||||||
|
xpcService!.markServiceReady()
|
||||||
|
}
|
||||||
|
#endif
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
if #available(iOS 18.0, *) {
|
if #available(iOS 18.0, *) {
|
||||||
ControlCenter.shared.reloadControls(ofKind: ExtensionProfile.controlKind)
|
ControlCenter.shared.reloadControls(ofKind: ExtensionProfile.controlKind)
|
||||||
@@ -68,48 +171,28 @@ open class ExtensionProvider: NEPacketTunnelProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func startService() async throws {
|
private func startService() async throws {
|
||||||
let profileID = await SharedPreferences.selectedProfileID.get()
|
guard let configContent = tunnelOptions?["configContent"] as? String else {
|
||||||
let profile: Profile?
|
throw ExtensionStartupError("(packet-tunnel) error: missing configContent in tunnel options")
|
||||||
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)")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let options = LibboxOverrideOptions()
|
let options = LibboxOverrideOptions()
|
||||||
do {
|
do {
|
||||||
try commandServer.startOrReloadService(configContent, options: options)
|
try commandServer!.startOrReloadService(configContent, options: options)
|
||||||
} catch {
|
} catch {
|
||||||
throw ExtensionStartupError("(packet-tunnel) error: start service: \(error.localizedDescription)")
|
throw ExtensionStartupError("(packet-tunnel) error: start service: \(error.localizedDescription)")
|
||||||
}
|
}
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
await SharedPreferences.startedByUser.set(true)
|
if !Variant.useSystemExtension, commandServer!.needWIFIState() {
|
||||||
if commandServer.needWIFIState() {
|
locationManager = CLLocationManager()
|
||||||
if !Variant.useSystemExtension {
|
locationDelegate = stubLocationDelegate()
|
||||||
locationManager = CLLocationManager()
|
locationManager?.delegate = locationDelegate
|
||||||
locationDelegate = stubLocationDelegate()
|
locationManager?.requestLocation()
|
||||||
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.")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
|
|
||||||
private var locationManager: CLLocationManager?
|
|
||||||
private var locationDelegate: stubLocationDelegate?
|
|
||||||
|
|
||||||
class stubLocationDelegate: NSObject, CLLocationManagerDelegate {
|
class stubLocationDelegate: NSObject, CLLocationManagerDelegate {
|
||||||
func locationManagerDidChangeAuthorization(_: CLLocationManager) {}
|
func locationManagerDidChangeAuthorization(_: CLLocationManager) {}
|
||||||
|
|
||||||
@@ -122,9 +205,9 @@ open class ExtensionProvider: NEPacketTunnelProvider {
|
|||||||
|
|
||||||
func stopService() {
|
func stopService() {
|
||||||
do {
|
do {
|
||||||
try commandServer.closeService()
|
try commandServer?.closeService()
|
||||||
} catch {
|
} catch {
|
||||||
writeMessage("(packet-tunnel) error: stop service: \(error.localizedDescription)")
|
writeMessage("(packet-tunnel) stop service: \(error.localizedDescription)")
|
||||||
}
|
}
|
||||||
if let platformInterface {
|
if let platformInterface {
|
||||||
platformInterface.reset()
|
platformInterface.reset()
|
||||||
@@ -149,9 +232,15 @@ open class ExtensionProvider: NEPacketTunnelProvider {
|
|||||||
commandServer = nil
|
commandServer = nil
|
||||||
}
|
}
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
if reason == .userInitiated {
|
if Variant.useSystemExtension {
|
||||||
await SharedPreferences.startedByUser.set(reason == .userInitiated)
|
xpcListener?.invalidate()
|
||||||
|
xpcListener = nil
|
||||||
|
xpcService?.commandServer = nil
|
||||||
|
xpcService = nil
|
||||||
|
UserServiceEndpointRegistry.shared.clear()
|
||||||
}
|
}
|
||||||
|
locationManager = nil
|
||||||
|
locationDelegate = nil
|
||||||
#endif
|
#endif
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
if #available(iOS 18.0, *) {
|
if #available(iOS 18.0, *) {
|
||||||
@@ -161,7 +250,15 @@ open class ExtensionProvider: NEPacketTunnelProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override open func handleAppMessage(_ messageData: Data) async -> Data? {
|
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 {
|
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)
|
#if os(macOS)
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import os
|
||||||
import SystemExtensions
|
import SystemExtensions
|
||||||
|
|
||||||
|
private let logger = Logger(category: "SystemExtension")
|
||||||
|
|
||||||
public class SystemExtension: NSObject, OSSystemExtensionRequestDelegate {
|
public class SystemExtension: NSObject, OSSystemExtensionRequestDelegate {
|
||||||
private let forceUpdate: Bool
|
private let forceUpdate: Bool
|
||||||
private let inBackground: Bool
|
private let inBackground: Bool
|
||||||
@@ -26,10 +29,10 @@
|
|||||||
existing.bundleVersion == ext.bundleVersion,
|
existing.bundleVersion == ext.bundleVersion,
|
||||||
existing.bundleShortVersion == ext.bundleShortVersion
|
existing.bundleShortVersion == ext.bundleShortVersion
|
||||||
{
|
{
|
||||||
NSLog("Skip update system extension")
|
logger.info("Skip update system extension")
|
||||||
return .cancel
|
return .cancel
|
||||||
} else {
|
} else {
|
||||||
NSLog("Update system extension")
|
logger.info("Update system extension")
|
||||||
return .replace
|
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
|
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 extensionBundleID: String { "\(packageName).extension" }
|
||||||
public static var systemExtensionBundleID: String { "\(packageName).system" }
|
public static var systemExtensionBundleID: String { "\(packageName).system" }
|
||||||
public static var fileProviderDomainID: String { "\(packageName).workingdir" }
|
public static var fileProviderDomainID: String { "\(packageName).workingdir" }
|
||||||
@@ -22,4 +29,9 @@ public enum AppConfiguration {
|
|||||||
public static var profileUTType: String { "\(packageName).profile" }
|
public static var profileUTType: String { "\(packageName).profile" }
|
||||||
public static var backgroundTaskID: String { "\(packageName).update_profiles" }
|
public static var backgroundTaskID: String { "\(packageName).update_profiles" }
|
||||||
public static var iCloudContainerID: String { "iCloud.\(packageName)" }
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -200,6 +200,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"App Settings" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Append `0.0.0.0/31` and `::/127` to `route_exclude_address` if not exists." : {
|
"Append `0.0.0.0/31` and `::/127` to `route_exclude_address` if not exists." : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -979,6 +982,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"Enable" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"enforceRoutes" : {
|
"enforceRoutes" : {
|
||||||
"shouldTranslate" : false
|
"shouldTranslate" : false
|
||||||
@@ -1168,6 +1174,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"Helper Service" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Helper Service Required" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Hide VPN Icon" : {
|
"Hide VPN Icon" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -1341,6 +1353,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"In the standalone version of SFM, reading WiFi state requires this app to be running. After you quit the SFM app, the sing-box service cannot continue to provide `wifi_ssid` and `wifi_bssid` routing rules." : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Inbound" : {
|
"Inbound" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -1364,6 +1379,9 @@
|
|||||||
},
|
},
|
||||||
"includeAllNetworks" : {
|
"includeAllNetworks" : {
|
||||||
"shouldTranslate" : false
|
"shouldTranslate" : false
|
||||||
|
},
|
||||||
|
"Install" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Install Network Extension" : {
|
"Install Network Extension" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -1530,6 +1548,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"Managing working directory requires Helper Service." : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Match Domains" : {
|
"Match Domains" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -2428,6 +2449,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"The sing-box service requires Helper Service to provide process lookup functionality, which supports `process_name` and `process_path` routing rules." : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"This app needs to be placed under the Applications folder to work." : {
|
"This app needs to be placed under the Applications folder to work." : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -2438,6 +2462,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"This helper service provides process lookup for `process_name` and `process_path` routing rules, and manages the working directory." : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"To Clipboard" : {
|
"To Clipboard" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -2508,6 +2535,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"Unavailable" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Uninstall" : {
|
"Uninstall" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -2631,6 +2661,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"WiFi State Access" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Working Directory" : {
|
"Working Directory" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ public struct MainView: View {
|
|||||||
@StateObject private var viewModel = MainViewModel()
|
@StateObject private var viewModel = MainViewModel()
|
||||||
@State private var showCardManagement = false
|
@State private var showCardManagement = false
|
||||||
@State private var cardConfigurationVersion = 0
|
@State private var cardConfigurationVersion = 0
|
||||||
|
@State private var settingsNavigationPath = NavigationPath()
|
||||||
|
@State private var pendingSettingsPage: SettingsPage?
|
||||||
|
|
||||||
private let profileEditor: (Binding<String>, Bool) -> AnyView = { text, isEditable in
|
private let profileEditor: (Binding<String>, Bool) -> AnyView = { text, isEditable in
|
||||||
AnyView(CodeEditTextView(text: text, isEditable: isEditable))
|
AnyView(CodeEditTextView(text: text, isEditable: isEditable))
|
||||||
@@ -21,11 +23,12 @@ public struct MainView: View {
|
|||||||
SidebarView(selection: $viewModel.selection)
|
SidebarView(selection: $viewModel.selection)
|
||||||
.navigationSplitViewColumnWidth(150)
|
.navigationSplitViewColumnWidth(150)
|
||||||
} detail: {
|
} detail: {
|
||||||
NavigationStack {
|
NavigationStack(path: $settingsNavigationPath) {
|
||||||
viewModel.selection.contentView
|
viewModel.selection.contentView
|
||||||
.navigationTitle(viewModel.selection.title)
|
.navigationTitle(viewModel.selection.title)
|
||||||
}
|
}
|
||||||
.environment(\.cardConfigurationVersion, cardConfigurationVersion)
|
.environment(\.cardConfigurationVersion, cardConfigurationVersion)
|
||||||
|
.environment(\.settingsNavigationPath, $settingsNavigationPath)
|
||||||
.navigationSplitViewColumnWidth(650)
|
.navigationSplitViewColumnWidth(650)
|
||||||
}
|
}
|
||||||
.frame(minHeight: 500)
|
.frame(minHeight: 500)
|
||||||
@@ -33,6 +36,7 @@ public struct MainView: View {
|
|||||||
viewModel.onAppear(environments: environments)
|
viewModel.onAppear(environments: environments)
|
||||||
}
|
}
|
||||||
.alert($viewModel.alert)
|
.alert($viewModel.alert)
|
||||||
|
.globalChecks()
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItem(placement: .navigation) {
|
ToolbarItem(placement: .navigation) {
|
||||||
StartStopButton()
|
StartStopButton()
|
||||||
@@ -57,10 +61,32 @@ public struct MainView: View {
|
|||||||
}
|
}
|
||||||
.onChangeCompat(of: viewModel.selection) { value in
|
.onChangeCompat(of: viewModel.selection) { value in
|
||||||
viewModel.onSelectionChange(value, environments: environments)
|
viewModel.onSelectionChange(value, environments: environments)
|
||||||
|
if value != .settings {
|
||||||
|
settingsNavigationPath = NavigationPath()
|
||||||
|
pendingSettingsPage = nil
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if let page = pendingSettingsPage {
|
||||||
|
settingsNavigationPath = NavigationPath()
|
||||||
|
settingsNavigationPath.append(page)
|
||||||
|
pendingSettingsPage = nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.onReceive(environments.openSettings) {
|
.onReceive(environments.openSettings) {
|
||||||
viewModel.openSettings()
|
viewModel.openSettings()
|
||||||
}
|
}
|
||||||
|
.onReceive(NotificationCenter.default.publisher(for: .navigateToSettingsPage)) { notification in
|
||||||
|
guard let page = notification.object as? SettingsPage else { return }
|
||||||
|
pendingSettingsPage = page
|
||||||
|
if viewModel.selection == .settings {
|
||||||
|
settingsNavigationPath = NavigationPath()
|
||||||
|
settingsNavigationPath.append(page)
|
||||||
|
pendingSettingsPage = nil
|
||||||
|
} else {
|
||||||
|
viewModel.selection = .settings
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.environment(\.selection, $viewModel.selection)
|
||||||
.environment(\.importProfile, $viewModel.importProfile)
|
.environment(\.importProfile, $viewModel.importProfile)
|
||||||
.environment(\.importRemoteProfile, $viewModel.importRemoteProfile)
|
.environment(\.importRemoteProfile, $viewModel.importRemoteProfile)
|
||||||
.environment(\.profileEditor, profileEditor)
|
.environment(\.profileEditor, profileEditor)
|
||||||
|
|||||||
@@ -39,11 +39,8 @@ public class MainViewModel: BaseViewModel {
|
|||||||
if url.host == "import-remote-profile" {
|
if url.host == "import-remote-profile" {
|
||||||
var error: NSError?
|
var error: NSError?
|
||||||
importRemoteProfile = LibboxParseRemoteProfileImportLink(url.absoluteString, &error)
|
importRemoteProfile = LibboxParseRemoteProfileImportLink(url.absoluteString, &error)
|
||||||
if error != nil {
|
if let error {
|
||||||
return
|
alert = AlertState(error: error)
|
||||||
}
|
|
||||||
if selection != .dashboard {
|
|
||||||
selection = .dashboard
|
|
||||||
}
|
}
|
||||||
} else if url.pathExtension == "bpf" {
|
} else if url.pathExtension == "bpf" {
|
||||||
Task {
|
Task {
|
||||||
@@ -61,10 +58,6 @@ public class MainViewModel: BaseViewModel {
|
|||||||
url.stopAccessingSecurityScopedResource()
|
url.stopAccessingSecurityScopedResource()
|
||||||
} catch {
|
} catch {
|
||||||
alert = AlertState(error: error)
|
alert = AlertState(error: error)
|
||||||
return
|
|
||||||
}
|
|
||||||
if selection != .dashboard {
|
|
||||||
selection = .dashboard
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -192,16 +192,12 @@ public struct MenuView: View {
|
|||||||
environments.selectedProfileUpdate.send()
|
environments.selectedProfileUpdate.send()
|
||||||
if profile.status.isConnected {
|
if profile.status.isConnected {
|
||||||
do {
|
do {
|
||||||
try await serviceReload()
|
try await profile.reloadService()
|
||||||
} catch {
|
} catch {
|
||||||
alert = AlertState(error: error)
|
alert = AlertState(error: error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
reasserting = false
|
reasserting = false
|
||||||
}
|
}
|
||||||
|
|
||||||
private nonisolated func serviceReload() async throws {
|
|
||||||
try LibboxNewStandaloneCommandClient()!.serviceReload()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ archive_ios:
|
|||||||
upload_ios:
|
upload_ios:
|
||||||
xcodebuild -exportArchive -archivePath build/SFI.xcarchive -exportOptionsPlist SFI/Upload.plist -allowProvisioningUpdates
|
xcodebuild -exportArchive -archivePath build/SFI.xcarchive -exportOptionsPlist SFI/Upload.plist -allowProvisioningUpdates
|
||||||
|
|
||||||
release_maocs: archive_macos upload_macos
|
release_macos: archive_macos upload_macos
|
||||||
|
|
||||||
archive_macos:
|
archive_macos:
|
||||||
xcodebuild archive -scheme SFM -configuration Release -archivePath build/SFM.xcarchive -allowProvisioningUpdates | xcbeautify | grep -A 10 -e "Archive Succeeded" -e " ARCHIVE FAILED" -e "❌"
|
xcodebuild archive -scheme SFM -configuration Release -archivePath build/SFM.xcarchive -allowProvisioningUpdates | xcbeautify | grep -A 10 -e "Archive Succeeded" -e " ARCHIVE FAILED" -e "❌"
|
||||||
|
|||||||
+1
-8
@@ -123,6 +123,7 @@ struct MainView: View {
|
|||||||
environments.postReload()
|
environments.postReload()
|
||||||
}
|
}
|
||||||
.alert($alert)
|
.alert($alert)
|
||||||
|
.globalChecks()
|
||||||
.onChangeCompat(of: scenePhase) { newValue in
|
.onChangeCompat(of: scenePhase) { newValue in
|
||||||
if newValue == .active {
|
if newValue == .active {
|
||||||
environments.postReload()
|
environments.postReload()
|
||||||
@@ -185,10 +186,6 @@ struct MainView: View {
|
|||||||
importRemoteProfile = LibboxParseRemoteProfileImportLink(url.absoluteString, &error)
|
importRemoteProfile = LibboxParseRemoteProfileImportLink(url.absoluteString, &error)
|
||||||
if let error {
|
if let error {
|
||||||
alert = AlertState(error: error)
|
alert = AlertState(error: error)
|
||||||
return
|
|
||||||
}
|
|
||||||
if selection != .dashboard {
|
|
||||||
selection = .dashboard
|
|
||||||
}
|
}
|
||||||
} else if url.pathExtension == "bpf" {
|
} else if url.pathExtension == "bpf" {
|
||||||
do {
|
do {
|
||||||
@@ -197,10 +194,6 @@ struct MainView: View {
|
|||||||
url.stopAccessingSecurityScopedResource()
|
url.stopAccessingSecurityScopedResource()
|
||||||
} catch {
|
} catch {
|
||||||
alert = AlertState(error: error)
|
alert = AlertState(error: error)
|
||||||
return
|
|
||||||
}
|
|
||||||
if selection != .dashboard {
|
|
||||||
selection = .dashboard
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
alert = AlertState(errorMessage: String(localized: "Handled unknown URL \(url.absoluteString)"))
|
alert = AlertState(errorMessage: String(localized: "Handled unknown URL \(url.absoluteString)"))
|
||||||
|
|||||||
@@ -113,5 +113,13 @@
|
|||||||
<string>As a universal proxy platform, sing-box configures routing according to your configuration.</string>
|
<string>As a universal proxy platform, sing-box configures routing according to your configuration.</string>
|
||||||
<key>NSCameraUsageDescription</key>
|
<key>NSCameraUsageDescription</key>
|
||||||
<string>Camera access is required to scan QR codes for importing profiles.</string>
|
<string>Camera access is required to scan QR codes for importing profiles.</string>
|
||||||
|
<key>NSLocationUsageDescription</key>
|
||||||
|
<string>sing-box uses the Location permission to provide users with routing based on WIFI SSID and BSSID rules, without reading your location.</string>
|
||||||
|
<key>NSLocationWhenInUseUsageDescription</key>
|
||||||
|
<string>sing-box uses the Location permission to provide users with routing based on WIFI SSID and BSSID rules, without reading your location.</string>
|
||||||
|
<key>NSLocationAlwaysUsageDescription</key>
|
||||||
|
<string>sing-box uses the Location permission to provide users with routing based on WIFI SSID and BSSID rules, without reading your location.</string>
|
||||||
|
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
|
||||||
|
<string>sing-box uses the Location permission to provide users with routing based on WIFI SSID and BSSID rules, without reading your location.</string>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -20,17 +20,13 @@
|
|||||||
<array>
|
<array>
|
||||||
<string>iCloud.io.nekohasekai.sfavt</string>
|
<string>iCloud.io.nekohasekai.sfavt</string>
|
||||||
</array>
|
</array>
|
||||||
<key>com.apple.security.app-sandbox</key>
|
|
||||||
<true/>
|
|
||||||
<key>com.apple.security.application-groups</key>
|
<key>com.apple.security.application-groups</key>
|
||||||
<array>
|
<array>
|
||||||
<string>$(TeamIdentifierPrefix)$(BASE_PACKAGE_IDENTIFIER)</string>
|
<string>$(TeamIdentifierPrefix)$(BASE_PACKAGE_IDENTIFIER)</string>
|
||||||
</array>
|
</array>
|
||||||
<key>com.apple.security.files.user-selected.read-write</key>
|
<key>com.apple.developer.networking.wifi-info</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.network.client</key>
|
<key>com.apple.security.personal-information.location</key>
|
||||||
<true/>
|
|
||||||
<key>com.apple.security.device.camera</key>
|
|
||||||
<true/>
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import AppKit
|
import AppKit
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import Libbox
|
||||||
import Library
|
import Library
|
||||||
import MacLibrary
|
import MacLibrary
|
||||||
|
|
||||||
@@ -7,6 +8,8 @@ class StandaloneApplicationDelegate: ApplicationDelegate {
|
|||||||
func applicationWillFinishLaunching(_: Notification) {
|
func applicationWillFinishLaunching(_: Notification) {
|
||||||
Variant.useSystemExtension = true
|
Variant.useSystemExtension = true
|
||||||
Variant.isBeta = false
|
Variant.isBeta = false
|
||||||
|
LibboxSetXPCDialer(CommandXPCDialer.shared)
|
||||||
|
UserServiceEndpointPublisher.shared.start()
|
||||||
Task {
|
Task {
|
||||||
await setupSystemExtension()
|
await setupSystemExtension()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ struct MainView: View {
|
|||||||
@Environment(\.scenePhase) private var scenePhase
|
@Environment(\.scenePhase) private var scenePhase
|
||||||
@EnvironmentObject private var environments: ExtensionEnvironments
|
@EnvironmentObject private var environments: ExtensionEnvironments
|
||||||
@State private var selection = NavigationPage.dashboard
|
@State private var selection = NavigationPage.dashboard
|
||||||
|
@State private var importProfile: LibboxProfileContent?
|
||||||
|
@State private var importRemoteProfile: LibboxImportRemoteProfile?
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
TabView(selection: $selection) {
|
TabView(selection: $selection) {
|
||||||
@@ -32,6 +34,9 @@ struct MainView: View {
|
|||||||
environments.connect()
|
environments.connect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.globalChecks()
|
||||||
.environment(\.selection, $selection)
|
.environment(\.selection, $selection)
|
||||||
|
.environment(\.importProfile, $importProfile)
|
||||||
|
.environment(\.importRemoteProfile, $importRemoteProfile)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,30 +1,4 @@
|
|||||||
import Libbox
|
|
||||||
import Library
|
import Library
|
||||||
import NetworkExtension
|
import NetworkExtension
|
||||||
import System
|
|
||||||
|
|
||||||
class PacketTunnelProvider: ExtensionProvider {
|
class PacketTunnelProvider: ExtensionProvider {}
|
||||||
override func startTunnel(options: [String: NSObject]?) async throws {
|
|
||||||
guard let usernameObject = options?["username"] else {
|
|
||||||
throw ExtensionStartupError("missing start options")
|
|
||||||
}
|
|
||||||
let username = usernameObject as! NSString
|
|
||||||
FilePath.sharedDirectory = URL(filePath: "/Users/\(username)/Library/Group Containers/\(FilePath.groupName)")
|
|
||||||
FilePath.iCloudDirectory = URL(filePath: "/Users/\(username)/Library/Mobile Documents/iCloud~\(FilePath.packageName.replacingOccurrences(of: ".", with: "~"))").appendingPathComponent("Documents", isDirectory: true)
|
|
||||||
let databasePath = FilePath.sharedDirectory.appendingPathComponent("settings.db").relativePath
|
|
||||||
if !FileManager.default.isReadableFile(atPath: databasePath) {
|
|
||||||
do {
|
|
||||||
let fd = try FileDescriptor.open(databasePath, .readOnly)
|
|
||||||
try! fd.close()
|
|
||||||
NSLog("Can access \(databasePath)")
|
|
||||||
} catch {
|
|
||||||
NSLog("Can't access \(databasePath): \(error.localizedDescription)")
|
|
||||||
}
|
|
||||||
try await Task.sleep(nanoseconds: NSEC_PER_MSEC * 100)
|
|
||||||
throw FullDiskAccessPermissionRequired.error
|
|
||||||
}
|
|
||||||
|
|
||||||
self.username = String(username)
|
|
||||||
try await super.startTunnel(options: options)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -8,11 +8,11 @@
|
|||||||
<array>
|
<array>
|
||||||
<string>packet-tunnel-provider-systemextension</string>
|
<string>packet-tunnel-provider-systemextension</string>
|
||||||
</array>
|
</array>
|
||||||
|
<key>com.apple.security.app-sandbox</key>
|
||||||
|
<true/>
|
||||||
<key>com.apple.security.application-groups</key>
|
<key>com.apple.security.application-groups</key>
|
||||||
<array>
|
<array>
|
||||||
<string>$(TeamIdentifierPrefix)$(BASE_PACKAGE_IDENTIFIER)</string>
|
<string>$(TeamIdentifierPrefix)$(BASE_PACKAGE_IDENTIFIER)</string>
|
||||||
</array>
|
</array>
|
||||||
<key>com.apple.security.app-sandbox</key>
|
|
||||||
<false/>
|
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -42,6 +42,9 @@
|
|||||||
3AE396072C21A60C00647718 /* Library.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3AEC211D2A459B4700A63465 /* Library.framework */; };
|
3AE396072C21A60C00647718 /* Library.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3AEC211D2A459B4700A63465 /* Library.framework */; };
|
||||||
3AE4D0BD2A6E2DDC009FEA9E /* Library.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3AEC211D2A459B4700A63465 /* Library.framework */; };
|
3AE4D0BD2A6E2DDC009FEA9E /* Library.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3AEC211D2A459B4700A63465 /* Library.framework */; };
|
||||||
3AE4D0BE2A6E2DDC009FEA9E /* Library.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3AEC211D2A459B4700A63465 /* Library.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
3AE4D0BE2A6E2DDC009FEA9E /* Library.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3AEC211D2A459B4700A63465 /* Library.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||||
|
3AE595822F08C3D400C13426 /* Library.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3AEC211D2A459B4700A63465 /* Library.framework */; };
|
||||||
|
3AE595AA2F08C61600C13426 /* RootHelper in Copy Helpers */ = {isa = PBXBuildFile; fileRef = 3AE595712F08C34000C13426 /* RootHelper */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
|
||||||
|
3AE595B02F08D00000C13426 /* HelperService/LaunchDaemons/io.nekohasekai.sfavt.helper.plist in Copy LaunchDaemon */ = {isa = PBXBuildFile; fileRef = 3AE595AE2F08D00000C13426 /* HelperService/LaunchDaemons/io.nekohasekai.sfavt.helper.plist */; };
|
||||||
3AEAEE992A4F16430059612D /* Extension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 3A096F862A4ED3DE00D4A2ED /* Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
3AEAEE992A4F16430059612D /* Extension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 3A096F862A4ED3DE00D4A2ED /* Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||||
3AEECBF22A6DF40A006A0E0C /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3AF342B12A4AA520002B34AC /* NetworkExtension.framework */; };
|
3AEECBF22A6DF40A006A0E0C /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3AF342B12A4AA520002B34AC /* NetworkExtension.framework */; };
|
||||||
3AEECC1A2A6DFA79006A0E0C /* io.nekohasekai.sfavt.system.systemextension in Embed System Extensions */ = {isa = PBXBuildFile; fileRef = 3AEECBF12A6DF40A006A0E0C /* io.nekohasekai.sfavt.system.systemextension */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
3AEECC1A2A6DFA79006A0E0C /* io.nekohasekai.sfavt.system.systemextension in Embed System Extensions */ = {isa = PBXBuildFile; fileRef = 3AEECBF12A6DF40A006A0E0C /* io.nekohasekai.sfavt.system.systemextension */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||||
@@ -183,6 +186,20 @@
|
|||||||
remoteGlobalIDString = 3AEC211C2A459B4700A63465;
|
remoteGlobalIDString = 3AEC211C2A459B4700A63465;
|
||||||
remoteInfo = Library;
|
remoteInfo = Library;
|
||||||
};
|
};
|
||||||
|
3AE595842F08C3D400C13426 /* PBXContainerItemProxy */ = {
|
||||||
|
isa = PBXContainerItemProxy;
|
||||||
|
containerPortal = 3AEC20BD2A45991900A63465 /* Project object */;
|
||||||
|
proxyType = 1;
|
||||||
|
remoteGlobalIDString = 3AEC211C2A459B4700A63465;
|
||||||
|
remoteInfo = Library;
|
||||||
|
};
|
||||||
|
3AE595A52F08C5C700C13426 /* PBXContainerItemProxy */ = {
|
||||||
|
isa = PBXContainerItemProxy;
|
||||||
|
containerPortal = 3AEC20BD2A45991900A63465 /* Project object */;
|
||||||
|
proxyType = 1;
|
||||||
|
remoteGlobalIDString = 3AE595702F08C34000C13426;
|
||||||
|
remoteInfo = RootHelper;
|
||||||
|
};
|
||||||
3AEAEE9A2A4F16430059612D /* PBXContainerItemProxy */ = {
|
3AEAEE9A2A4F16430059612D /* PBXContainerItemProxy */ = {
|
||||||
isa = PBXContainerItemProxy;
|
isa = PBXContainerItemProxy;
|
||||||
containerPortal = 3AEC20BD2A45991900A63465 /* Project object */;
|
containerPortal = 3AEC20BD2A45991900A63465 /* Project object */;
|
||||||
@@ -343,6 +360,37 @@
|
|||||||
name = "Embed Frameworks";
|
name = "Embed Frameworks";
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
3AE5956F2F08C34000C13426 /* CopyFiles */ = {
|
||||||
|
isa = PBXCopyFilesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
dstPath = /usr/share/man/man1/;
|
||||||
|
dstSubfolderSpec = 0;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 1;
|
||||||
|
};
|
||||||
|
3AE595A92F08C5E200C13426 /* Copy Helpers */ = {
|
||||||
|
isa = PBXCopyFilesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
dstPath = ../Helpers;
|
||||||
|
dstSubfolderSpec = 6;
|
||||||
|
files = (
|
||||||
|
3AE595AA2F08C61600C13426 /* RootHelper in Copy Helpers */,
|
||||||
|
);
|
||||||
|
name = "Copy Helpers";
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
3AE595AC2F08C63000C13426 /* Copy LaunchDaemon */ = {
|
||||||
|
isa = PBXCopyFilesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
dstPath = ../Library/LaunchDaemons;
|
||||||
|
dstSubfolderSpec = 7;
|
||||||
|
files = (
|
||||||
|
3AE595B02F08D00000C13426 /* HelperService/LaunchDaemons/io.nekohasekai.sfavt.helper.plist in Copy LaunchDaemon */,
|
||||||
|
);
|
||||||
|
name = "Copy LaunchDaemon";
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
3AEECC212A6DFA79006A0E0C /* Embed Frameworks */ = {
|
3AEECC212A6DFA79006A0E0C /* Embed Frameworks */ = {
|
||||||
isa = PBXCopyFilesBuildPhase;
|
isa = PBXCopyFilesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
@@ -384,6 +432,11 @@
|
|||||||
3AE395F22C21A5CA00647718 /* WidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = WidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
3AE395F22C21A5CA00647718 /* WidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = WidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
3AE395F32C21A5CA00647718 /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; };
|
3AE395F32C21A5CA00647718 /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; };
|
||||||
3AE395F52C21A5CA00647718 /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; };
|
3AE395F52C21A5CA00647718 /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; };
|
||||||
|
3AE595712F08C34000C13426 /* RootHelper */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = RootHelper; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
3AE595A12F08C4E900C13426 /* CoreWLAN.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreWLAN.framework; path = System/Library/Frameworks/CoreWLAN.framework; sourceTree = SDKROOT; };
|
||||||
|
3AE595A32F08C4ED00C13426 /* UserNotifications.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UserNotifications.framework; path = System/Library/Frameworks/UserNotifications.framework; sourceTree = SDKROOT; };
|
||||||
|
3AE595AE2F08D00000C13426 /* HelperService/LaunchDaemons/io.nekohasekai.sfavt.helper.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = HelperService/LaunchDaemons/io.nekohasekai.sfavt.helper.plist; sourceTree = "<group>"; };
|
||||||
|
3AE595AF2F08D00000C13426 /* UserHelper/LaunchAgents/io.nekohasekai.sfavt.userhelper.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = UserHelper/LaunchAgents/io.nekohasekai.sfavt.userhelper.plist; sourceTree = "<group>"; };
|
||||||
3AEC20DB2A4599D000A63465 /* Libbox.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = Libbox.xcframework; sourceTree = "<group>"; };
|
3AEC20DB2A4599D000A63465 /* Libbox.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = Libbox.xcframework; sourceTree = "<group>"; };
|
||||||
3AEC20F32A459AB400A63465 /* sing-box.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "sing-box.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
3AEC20F32A459AB400A63465 /* sing-box.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "sing-box.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
3AEC21092A459B1900A63465 /* sing-box.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "sing-box.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
3AEC21092A459B1900A63465 /* sing-box.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "sing-box.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
@@ -545,6 +598,7 @@
|
|||||||
3ADDCFC62E8B72B4009ACE1D /* IntentsExtension */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (3ADDCFC92E8B72B4009ACE1D /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = IntentsExtension; sourceTree = "<group>"; };
|
3ADDCFC62E8B72B4009ACE1D /* IntentsExtension */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (3ADDCFC92E8B72B4009ACE1D /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = IntentsExtension; sourceTree = "<group>"; };
|
||||||
3ADDCFCD2E8B72B8009ACE1D /* TVExtension */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (3ADDCFCF2E8B72B8009ACE1D /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = TVExtension; sourceTree = "<group>"; };
|
3ADDCFCD2E8B72B8009ACE1D /* TVExtension */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (3ADDCFCF2E8B72B8009ACE1D /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = TVExtension; sourceTree = "<group>"; };
|
||||||
3ADDCFD52E8B72BC009ACE1D /* WidgetExtension */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (3ADDCFDA2E8B72BC009ACE1D /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = WidgetExtension; sourceTree = "<group>"; };
|
3ADDCFD52E8B72BC009ACE1D /* WidgetExtension */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (3ADDCFDA2E8B72BC009ACE1D /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = WidgetExtension; sourceTree = "<group>"; };
|
||||||
|
3AE595722F08C34000C13426 /* HelperService */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = HelperService; sourceTree = "<group>"; };
|
||||||
/* End PBXFileSystemSynchronizedRootGroup section */
|
/* End PBXFileSystemSynchronizedRootGroup section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
@@ -611,6 +665,14 @@
|
|||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
3AE5956E2F08C34000C13426 /* Frameworks */ = {
|
||||||
|
isa = PBXFrameworksBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
3AE595822F08C3D400C13426 /* Library.framework in Frameworks */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
3AEC20F02A459AB400A63465 /* Frameworks */ = {
|
3AEC20F02A459AB400A63465 /* Frameworks */ = {
|
||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
@@ -676,6 +738,15 @@
|
|||||||
/* End PBXFrameworksBuildPhase section */
|
/* End PBXFrameworksBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXGroup section */
|
/* Begin PBXGroup section */
|
||||||
|
3AE5C9042F08E53400C13426 /* Recovered References */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
3AE595AE2F08D00000C13426 /* HelperService/LaunchDaemons/io.nekohasekai.sfavt.helper.plist */,
|
||||||
|
3AE595AF2F08D00000C13426 /* UserHelper/LaunchAgents/io.nekohasekai.sfavt.userhelper.plist */,
|
||||||
|
);
|
||||||
|
name = "Recovered References";
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
3AEC20BC2A45991900A63465 = {
|
3AEC20BC2A45991900A63465 = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -694,8 +765,10 @@
|
|||||||
3ADDCFCD2E8B72B8009ACE1D /* TVExtension */,
|
3ADDCFCD2E8B72B8009ACE1D /* TVExtension */,
|
||||||
3ADDCFD52E8B72BC009ACE1D /* WidgetExtension */,
|
3ADDCFD52E8B72BC009ACE1D /* WidgetExtension */,
|
||||||
3AAAFB232EF5218F004C69AD /* FileProviderExtension */,
|
3AAAFB232EF5218F004C69AD /* FileProviderExtension */,
|
||||||
|
3AE595722F08C34000C13426 /* HelperService */,
|
||||||
3AEC20C72A45991900A63465 /* Products */,
|
3AEC20C72A45991900A63465 /* Products */,
|
||||||
3AEC21012A459AE300A63465 /* Frameworks */,
|
3AEC21012A459AE300A63465 /* Frameworks */,
|
||||||
|
3AE5C9042F08E53400C13426 /* Recovered References */,
|
||||||
);
|
);
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
@@ -715,6 +788,7 @@
|
|||||||
3AE171992A8128DD00393060 /* TVExtension.appex */,
|
3AE171992A8128DD00393060 /* TVExtension.appex */,
|
||||||
3AE395F22C21A5CA00647718 /* WidgetExtension.appex */,
|
3AE395F22C21A5CA00647718 /* WidgetExtension.appex */,
|
||||||
3AAAFB202EF5218F004C69AD /* FileProviderExtension.appex */,
|
3AAAFB202EF5218F004C69AD /* FileProviderExtension.appex */,
|
||||||
|
3AE595712F08C34000C13426 /* RootHelper */,
|
||||||
);
|
);
|
||||||
name = Products;
|
name = Products;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -722,6 +796,8 @@
|
|||||||
3AEC21012A459AE300A63465 /* Frameworks */ = {
|
3AEC21012A459AE300A63465 /* Frameworks */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
3AE595A32F08C4ED00C13426 /* UserNotifications.framework */,
|
||||||
|
3AE595A12F08C4E900C13426 /* CoreWLAN.framework */,
|
||||||
3AAAFE162EF52772004C69AD /* FileProvider.framework */,
|
3AAAFE162EF52772004C69AD /* FileProvider.framework */,
|
||||||
3AC72A2E2EED94DD0039DEA4 /* SystemConfiguration.framework */,
|
3AC72A2E2EED94DD0039DEA4 /* SystemConfiguration.framework */,
|
||||||
3AC72A302EED94F60039DEA4 /* SystemConfiguration.framework */,
|
3AC72A302EED94F60039DEA4 /* SystemConfiguration.framework */,
|
||||||
@@ -909,6 +985,29 @@
|
|||||||
productReference = 3AE395F22C21A5CA00647718 /* WidgetExtension.appex */;
|
productReference = 3AE395F22C21A5CA00647718 /* WidgetExtension.appex */;
|
||||||
productType = "com.apple.product-type.app-extension";
|
productType = "com.apple.product-type.app-extension";
|
||||||
};
|
};
|
||||||
|
3AE595702F08C34000C13426 /* RootHelper */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = 3AE595752F08C34000C13426 /* Build configuration list for PBXNativeTarget "RootHelper" */;
|
||||||
|
buildPhases = (
|
||||||
|
3AE5956D2F08C34000C13426 /* Sources */,
|
||||||
|
3AE5956E2F08C34000C13426 /* Frameworks */,
|
||||||
|
3AE5956F2F08C34000C13426 /* CopyFiles */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
3AE595852F08C3D400C13426 /* PBXTargetDependency */,
|
||||||
|
);
|
||||||
|
fileSystemSynchronizedGroups = (
|
||||||
|
3AE595722F08C34000C13426 /* HelperService */,
|
||||||
|
);
|
||||||
|
name = RootHelper;
|
||||||
|
packageProductDependencies = (
|
||||||
|
);
|
||||||
|
productName = RootHelper;
|
||||||
|
productReference = 3AE595712F08C34000C13426 /* RootHelper */;
|
||||||
|
productType = "com.apple.product-type.tool";
|
||||||
|
};
|
||||||
3AEC20F22A459AB400A63465 /* SFI */ = {
|
3AEC20F22A459AB400A63465 /* SFI */ = {
|
||||||
isa = PBXNativeTarget;
|
isa = PBXNativeTarget;
|
||||||
buildConfigurationList = 3AEC20FE2A459AB500A63465 /* Build configuration list for PBXNativeTarget "SFI" */;
|
buildConfigurationList = 3AEC20FE2A459AB500A63465 /* Build configuration list for PBXNativeTarget "SFI" */;
|
||||||
@@ -1023,10 +1122,13 @@
|
|||||||
3AEECC022A6DF9CA006A0E0C /* Resources */,
|
3AEECC022A6DF9CA006A0E0C /* Resources */,
|
||||||
3AEECC212A6DFA79006A0E0C /* Embed Frameworks */,
|
3AEECC212A6DFA79006A0E0C /* Embed Frameworks */,
|
||||||
3AEECC232A6DFA79006A0E0C /* Embed System Extensions */,
|
3AEECC232A6DFA79006A0E0C /* Embed System Extensions */,
|
||||||
|
3AE595A92F08C5E200C13426 /* Copy Helpers */,
|
||||||
|
3AE595AC2F08C63000C13426 /* Copy LaunchDaemon */,
|
||||||
);
|
);
|
||||||
buildRules = (
|
buildRules = (
|
||||||
);
|
);
|
||||||
dependencies = (
|
dependencies = (
|
||||||
|
3AE595A62F08C5C700C13426 /* PBXTargetDependency */,
|
||||||
3AEECC1C2A6DFA79006A0E0C /* PBXTargetDependency */,
|
3AEECC1C2A6DFA79006A0E0C /* PBXTargetDependency */,
|
||||||
3AEECC202A6DFA79006A0E0C /* PBXTargetDependency */,
|
3AEECC202A6DFA79006A0E0C /* PBXTargetDependency */,
|
||||||
3AEECC3D2A6DFDC6006A0E0C /* PBXTargetDependency */,
|
3AEECC3D2A6DFDC6006A0E0C /* PBXTargetDependency */,
|
||||||
@@ -1097,6 +1199,9 @@
|
|||||||
3AE395F12C21A5CA00647718 = {
|
3AE395F12C21A5CA00647718 = {
|
||||||
CreatedOnToolsVersion = 16.0;
|
CreatedOnToolsVersion = 16.0;
|
||||||
};
|
};
|
||||||
|
3AE595702F08C34000C13426 = {
|
||||||
|
CreatedOnToolsVersion = 26.2;
|
||||||
|
};
|
||||||
3AEC20F22A459AB400A63465 = {
|
3AEC20F22A459AB400A63465 = {
|
||||||
CreatedOnToolsVersion = 15.0;
|
CreatedOnToolsVersion = 15.0;
|
||||||
};
|
};
|
||||||
@@ -1155,6 +1260,7 @@
|
|||||||
3A77016C2A4E6B34008F031F /* IntentsExtension */,
|
3A77016C2A4E6B34008F031F /* IntentsExtension */,
|
||||||
3AE395F12C21A5CA00647718 /* WidgetExtension */,
|
3AE395F12C21A5CA00647718 /* WidgetExtension */,
|
||||||
3AAAFB1F2EF5218F004C69AD /* FileProviderExtension */,
|
3AAAFB1F2EF5218F004C69AD /* FileProviderExtension */,
|
||||||
|
3AE595702F08C34000C13426 /* RootHelper */,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
/* End PBXProject section */
|
/* End PBXProject section */
|
||||||
@@ -1296,6 +1402,13 @@
|
|||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
3AE5956D2F08C34000C13426 /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
3AEC20EF2A459AB400A63465 /* Sources */ = {
|
3AEC20EF2A459AB400A63465 /* Sources */ = {
|
||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
@@ -1426,6 +1539,16 @@
|
|||||||
target = 3AEC211C2A459B4700A63465 /* Library */;
|
target = 3AEC211C2A459B4700A63465 /* Library */;
|
||||||
targetProxy = 3AE4D0BA2A6E2C55009FEA9E /* PBXContainerItemProxy */;
|
targetProxy = 3AE4D0BA2A6E2C55009FEA9E /* PBXContainerItemProxy */;
|
||||||
};
|
};
|
||||||
|
3AE595852F08C3D400C13426 /* PBXTargetDependency */ = {
|
||||||
|
isa = PBXTargetDependency;
|
||||||
|
target = 3AEC211C2A459B4700A63465 /* Library */;
|
||||||
|
targetProxy = 3AE595842F08C3D400C13426 /* PBXContainerItemProxy */;
|
||||||
|
};
|
||||||
|
3AE595A62F08C5C700C13426 /* PBXTargetDependency */ = {
|
||||||
|
isa = PBXTargetDependency;
|
||||||
|
target = 3AE595702F08C34000C13426 /* RootHelper */;
|
||||||
|
targetProxy = 3AE595A52F08C5C700C13426 /* PBXContainerItemProxy */;
|
||||||
|
};
|
||||||
3AEAEE9B2A4F16430059612D /* PBXTargetDependency */ = {
|
3AEAEE9B2A4F16430059612D /* PBXTargetDependency */ = {
|
||||||
isa = PBXTargetDependency;
|
isa = PBXTargetDependency;
|
||||||
target = 3A096F852A4ED3DE00D4A2ED /* Extension */;
|
target = 3A096F852A4ED3DE00D4A2ED /* Extension */;
|
||||||
@@ -1980,6 +2103,86 @@
|
|||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
};
|
};
|
||||||
|
3AE595762F08C34000C13426 /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
APP_GROUP_IDENTIFIER = "$(TeamIdentifierPrefix)$(BASE_PACKAGE_IDENTIFIER)";
|
||||||
|
AUTOMATION_APPLE_EVENTS = NO;
|
||||||
|
CODE_SIGN_ENTITLEMENTS = HelperService/RootHelper.entitlements;
|
||||||
|
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CREATE_INFOPLIST_SECTION_IN_BINARY = YES;
|
||||||
|
DEVELOPMENT_TEAM = 287TTNZF8L;
|
||||||
|
ENABLE_HARDENED_RUNTIME = NO;
|
||||||
|
ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO;
|
||||||
|
ENABLE_RESOURCE_ACCESS_CALENDARS = NO;
|
||||||
|
ENABLE_RESOURCE_ACCESS_CAMERA = NO;
|
||||||
|
ENABLE_RESOURCE_ACCESS_CONTACTS = NO;
|
||||||
|
ENABLE_RESOURCE_ACCESS_LOCATION = NO;
|
||||||
|
ENABLE_RESOURCE_ACCESS_PHOTO_LIBRARY = NO;
|
||||||
|
INFOPLIST_FILE = HelperService/Info.plist;
|
||||||
|
INFOPLIST_KEY_WKSupportsLiveActivityLaunchAttributeTypes = "";
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/../Frameworks",
|
||||||
|
);
|
||||||
|
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = "$(BASE_PACKAGE_IDENTIFIER).helper";
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
RUNTIME_EXCEPTION_ALLOW_DYLD_ENVIRONMENT_VARIABLES = NO;
|
||||||
|
RUNTIME_EXCEPTION_ALLOW_JIT = NO;
|
||||||
|
RUNTIME_EXCEPTION_ALLOW_UNSIGNED_EXECUTABLE_MEMORY = NO;
|
||||||
|
RUNTIME_EXCEPTION_DEBUGGING_TOOL = NO;
|
||||||
|
RUNTIME_EXCEPTION_DISABLE_EXECUTABLE_PAGE_PROTECTION = NO;
|
||||||
|
RUNTIME_EXCEPTION_DISABLE_LIBRARY_VALIDATION = NO;
|
||||||
|
SDKROOT = macosx;
|
||||||
|
SKIP_INSTALL = YES;
|
||||||
|
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||||
|
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
3AE595772F08C34000C13426 /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
APP_GROUP_IDENTIFIER = "$(TeamIdentifierPrefix)$(BASE_PACKAGE_IDENTIFIER)";
|
||||||
|
AUTOMATION_APPLE_EVENTS = NO;
|
||||||
|
CODE_SIGN_ENTITLEMENTS = HelperService/RootHelper.entitlements;
|
||||||
|
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CREATE_INFOPLIST_SECTION_IN_BINARY = YES;
|
||||||
|
DEVELOPMENT_TEAM = 287TTNZF8L;
|
||||||
|
ENABLE_HARDENED_RUNTIME = NO;
|
||||||
|
ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO;
|
||||||
|
ENABLE_RESOURCE_ACCESS_CALENDARS = NO;
|
||||||
|
ENABLE_RESOURCE_ACCESS_CAMERA = NO;
|
||||||
|
ENABLE_RESOURCE_ACCESS_CONTACTS = NO;
|
||||||
|
ENABLE_RESOURCE_ACCESS_LOCATION = NO;
|
||||||
|
ENABLE_RESOURCE_ACCESS_PHOTO_LIBRARY = NO;
|
||||||
|
INFOPLIST_FILE = HelperService/Info.plist;
|
||||||
|
INFOPLIST_KEY_WKSupportsLiveActivityLaunchAttributeTypes = "";
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/../Frameworks",
|
||||||
|
);
|
||||||
|
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = "$(BASE_PACKAGE_IDENTIFIER).helper";
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
RUNTIME_EXCEPTION_ALLOW_DYLD_ENVIRONMENT_VARIABLES = NO;
|
||||||
|
RUNTIME_EXCEPTION_ALLOW_JIT = NO;
|
||||||
|
RUNTIME_EXCEPTION_ALLOW_UNSIGNED_EXECUTABLE_MEMORY = NO;
|
||||||
|
RUNTIME_EXCEPTION_DEBUGGING_TOOL = NO;
|
||||||
|
RUNTIME_EXCEPTION_DISABLE_EXECUTABLE_PAGE_PROTECTION = NO;
|
||||||
|
RUNTIME_EXCEPTION_DISABLE_LIBRARY_VALIDATION = NO;
|
||||||
|
SDKROOT = macosx;
|
||||||
|
SKIP_INSTALL = YES;
|
||||||
|
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||||
|
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
3AEC20CB2A45991900A63465 /* Debug */ = {
|
3AEC20CB2A45991900A63465 /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
@@ -2379,7 +2582,18 @@
|
|||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEVELOPMENT_TEAM = "";
|
DEVELOPMENT_TEAM = "";
|
||||||
"DEVELOPMENT_TEAM[sdk=macosx*]" = 287TTNZF8L;
|
"DEVELOPMENT_TEAM[sdk=macosx*]" = 287TTNZF8L;
|
||||||
|
ENABLE_APP_SANDBOX = YES;
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
|
ENABLE_INCOMING_NETWORK_CONNECTIONS = YES;
|
||||||
|
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
|
||||||
|
ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO;
|
||||||
|
ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO;
|
||||||
|
ENABLE_RESOURCE_ACCESS_CALENDARS = NO;
|
||||||
|
ENABLE_RESOURCE_ACCESS_CAMERA = NO;
|
||||||
|
ENABLE_RESOURCE_ACCESS_CONTACTS = NO;
|
||||||
|
ENABLE_RESOURCE_ACCESS_LOCATION = NO;
|
||||||
|
ENABLE_RESOURCE_ACCESS_PRINTING = NO;
|
||||||
|
ENABLE_RESOURCE_ACCESS_USB = NO;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = SystemExtension/Info.plist;
|
INFOPLIST_FILE = SystemExtension/Info.plist;
|
||||||
@@ -2416,7 +2630,18 @@
|
|||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEVELOPMENT_TEAM = "";
|
DEVELOPMENT_TEAM = "";
|
||||||
"DEVELOPMENT_TEAM[sdk=macosx*]" = 287TTNZF8L;
|
"DEVELOPMENT_TEAM[sdk=macosx*]" = 287TTNZF8L;
|
||||||
|
ENABLE_APP_SANDBOX = YES;
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
|
ENABLE_INCOMING_NETWORK_CONNECTIONS = YES;
|
||||||
|
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
|
||||||
|
ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO;
|
||||||
|
ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO;
|
||||||
|
ENABLE_RESOURCE_ACCESS_CALENDARS = NO;
|
||||||
|
ENABLE_RESOURCE_ACCESS_CAMERA = NO;
|
||||||
|
ENABLE_RESOURCE_ACCESS_CONTACTS = NO;
|
||||||
|
ENABLE_RESOURCE_ACCESS_LOCATION = NO;
|
||||||
|
ENABLE_RESOURCE_ACCESS_PRINTING = NO;
|
||||||
|
ENABLE_RESOURCE_ACCESS_USB = NO;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = SystemExtension/Info.plist;
|
INFOPLIST_FILE = SystemExtension/Info.plist;
|
||||||
@@ -2669,6 +2894,15 @@
|
|||||||
defaultConfigurationIsVisible = 0;
|
defaultConfigurationIsVisible = 0;
|
||||||
defaultConfigurationName = Release;
|
defaultConfigurationName = Release;
|
||||||
};
|
};
|
||||||
|
3AE595752F08C34000C13426 /* Build configuration list for PBXNativeTarget "RootHelper" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
3AE595762F08C34000C13426 /* Debug */,
|
||||||
|
3AE595772F08C34000C13426 /* Release */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
3AEC20C02A45991900A63465 /* Build configuration list for PBXProject "sing-box" */ = {
|
3AEC20C02A45991900A63465 /* Build configuration list for PBXProject "sing-box" */ = {
|
||||||
isa = XCConfigurationList;
|
isa = XCConfigurationList;
|
||||||
buildConfigurations = (
|
buildConfigurations = (
|
||||||
|
|||||||
Reference in New Issue
Block a user