From b83fcce3105081a9964fd0bbfd4f0c4d41479ff1 Mon Sep 17 00:00:00 2001 From: GFWFighter Date: Tue, 24 Oct 2023 18:29:53 +0330 Subject: [PATCH] First working version --- ios/Base.xcconfig | 2 +- ios/Runner.xcodeproj/project.pbxproj | 56 +++++- ios/Runner/AppDelegate.swift | 36 +++- ios/Runner/Handlers/AlertsEventHandler.swift | 45 +++++ ios/Runner/Handlers/FileMethodHandler.swift | 35 ++++ ios/Runner/Handlers/GroupsEventHandler.swift | 93 +++++++++ ios/Runner/Handlers/LogsEventHandler.swift | 28 +++ ios/Runner/Handlers/MethodHandler.swift | 180 ++++++++++++++++++ ios/Runner/Handlers/StatusEventHandler.swift | 46 +++++ ios/Runner/VPN/Helpers/Stored.swift | 86 +++++++++ ios/Runner/VPN/VPNConfig.swift | 22 +++ ios/Runner/VPN/VPNManager.swift | 15 ++ ios/Shared/FilePath.swift | 2 - ios/SingBoxPacketTunnel/Logger.swift | 52 +++++ .../PacketTunnelProvider.swift | 8 +- .../SingBox/ExtensionProvider.swift | 14 +- ios/SingBoxPacketTunnel/SingBox/SingBox.swift | 4 +- lib/services/files_editor_service.dart | 40 +++- lib/services/singbox/singbox_service.dart | 2 +- 19 files changed, 734 insertions(+), 32 deletions(-) create mode 100644 ios/Runner/Handlers/AlertsEventHandler.swift create mode 100644 ios/Runner/Handlers/FileMethodHandler.swift create mode 100644 ios/Runner/Handlers/GroupsEventHandler.swift create mode 100644 ios/Runner/Handlers/LogsEventHandler.swift create mode 100644 ios/Runner/Handlers/MethodHandler.swift create mode 100644 ios/Runner/Handlers/StatusEventHandler.swift create mode 100644 ios/Runner/VPN/Helpers/Stored.swift create mode 100644 ios/Runner/VPN/VPNConfig.swift create mode 100644 ios/SingBoxPacketTunnel/Logger.swift diff --git a/ios/Base.xcconfig b/ios/Base.xcconfig index d7bada1c..50d83a97 100644 --- a/ios/Base.xcconfig +++ b/ios/Base.xcconfig @@ -5,5 +5,5 @@ // Created by GFWFighter on 7/24/1402 AP. // -BASE_BUNDLE_IDENTIFIER=com.hiddify.next +BASE_BUNDLE_IDENTIFIER=com.hiddify.app DEVELOPMENT_TEAM=XXXXXXXXX diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index c9da0dc7..b1ec30a5 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -10,6 +10,16 @@ 032158B82ADDF8BF008D943B /* VPNManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 032158B72ADDF8BF008D943B /* VPNManager.swift */; }; 032158BA2ADDFCC9008D943B /* TrafficReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 032158B92ADDFCC9008D943B /* TrafficReader.swift */; }; 032158BC2ADDFD09008D943B /* SingBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 032158BB2ADDFD09008D943B /* SingBox.swift */; }; + 03B516672AE6B93A00EA47E2 /* MethodHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03B516662AE6B93A00EA47E2 /* MethodHandler.swift */; }; + 03B516692AE7306B00EA47E2 /* StatusEventHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03B516682AE7306B00EA47E2 /* StatusEventHandler.swift */; }; + 03B5166B2AE7315E00EA47E2 /* AlertsEventHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03B5166A2AE7315E00EA47E2 /* AlertsEventHandler.swift */; }; + 03B5166D2AE7325500EA47E2 /* LogsEventHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03B5166C2AE7325500EA47E2 /* LogsEventHandler.swift */; }; + 03B516712AE74CCD00EA47E2 /* VPNConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03B516702AE74CCD00EA47E2 /* VPNConfig.swift */; }; + 03B516742AE74D2200EA47E2 /* Stored.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03B516732AE74D2200EA47E2 /* Stored.swift */; }; + 03B516762AE762F700EA47E2 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03B516752AE762F700EA47E2 /* Logger.swift */; }; + 03B516772AE7634400EA47E2 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03B516752AE762F700EA47E2 /* Logger.swift */; }; + 03B5167B2AE79DB400EA47E2 /* FileMethodHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03B5167A2AE79DB400EA47E2 /* FileMethodHandler.swift */; }; + 03B5167D2AE7AC6200EA47E2 /* GroupsEventHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03B5167C2AE7AC6200EA47E2 /* GroupsEventHandler.swift */; }; 03E392B82ADDA00E000ADF15 /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 03E392B72ADDA00E000ADF15 /* NetworkExtension.framework */; }; 03E392BB2ADDA00F000ADF15 /* PacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03E392BA2ADDA00F000ADF15 /* PacketTunnelProvider.swift */; }; 03E392C02ADDA00F000ADF15 /* SingBoxPacketTunnel.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 03E392B62ADDA00E000ADF15 /* SingBoxPacketTunnel.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; @@ -75,6 +85,15 @@ 032158B72ADDF8BF008D943B /* VPNManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNManager.swift; sourceTree = ""; }; 032158B92ADDFCC9008D943B /* TrafficReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrafficReader.swift; sourceTree = ""; }; 032158BB2ADDFD09008D943B /* SingBox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingBox.swift; sourceTree = ""; }; + 03B516662AE6B93A00EA47E2 /* MethodHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MethodHandler.swift; sourceTree = ""; }; + 03B516682AE7306B00EA47E2 /* StatusEventHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusEventHandler.swift; sourceTree = ""; }; + 03B5166A2AE7315E00EA47E2 /* AlertsEventHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertsEventHandler.swift; sourceTree = ""; }; + 03B5166C2AE7325500EA47E2 /* LogsEventHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogsEventHandler.swift; sourceTree = ""; }; + 03B516702AE74CCD00EA47E2 /* VPNConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNConfig.swift; sourceTree = ""; }; + 03B516732AE74D2200EA47E2 /* Stored.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Stored.swift; sourceTree = ""; }; + 03B516752AE762F700EA47E2 /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = ""; }; + 03B5167A2AE79DB400EA47E2 /* FileMethodHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileMethodHandler.swift; sourceTree = ""; }; + 03B5167C2AE7AC6200EA47E2 /* GroupsEventHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupsEventHandler.swift; sourceTree = ""; }; 03E392B62ADDA00E000ADF15 /* SingBoxPacketTunnel.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = SingBoxPacketTunnel.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 03E392B72ADDA00E000ADF15 /* NetworkExtension.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = NetworkExtension.framework; path = System/Library/Frameworks/NetworkExtension.framework; sourceTree = SDKROOT; }; 03E392BA2ADDA00F000ADF15 /* PacketTunnelProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelProvider.swift; sourceTree = ""; }; @@ -144,17 +163,41 @@ 032158B62ADDF8AF008D943B /* VPN */ = { isa = PBXGroup; children = ( + 03B516722AE74D1700EA47E2 /* Helpers */, 032158B72ADDF8BF008D943B /* VPNManager.swift */, + 03B516702AE74CCD00EA47E2 /* VPNConfig.swift */, ); path = VPN; sourceTree = ""; }; + 03B5166E2AE7325D00EA47E2 /* Handlers */ = { + isa = PBXGroup; + children = ( + 03B516662AE6B93A00EA47E2 /* MethodHandler.swift */, + 03B5167A2AE79DB400EA47E2 /* FileMethodHandler.swift */, + 03B516682AE7306B00EA47E2 /* StatusEventHandler.swift */, + 03B5166A2AE7315E00EA47E2 /* AlertsEventHandler.swift */, + 03B5166C2AE7325500EA47E2 /* LogsEventHandler.swift */, + 03B5167C2AE7AC6200EA47E2 /* GroupsEventHandler.swift */, + ); + path = Handlers; + sourceTree = ""; + }; + 03B516722AE74D1700EA47E2 /* Helpers */ = { + isa = PBXGroup; + children = ( + 03B516732AE74D2200EA47E2 /* Stored.swift */, + ); + path = Helpers; + sourceTree = ""; + }; 03E392B92ADDA00F000ADF15 /* SingBoxPacketTunnel */ = { isa = PBXGroup; children = ( 03E392CA2ADDE063000ADF15 /* SingBox */, 03E392BA2ADDA00F000ADF15 /* PacketTunnelProvider.swift */, 032158B92ADDFCC9008D943B /* TrafficReader.swift */, + 03B516752AE762F700EA47E2 /* Logger.swift */, 03E392BC2ADDA00F000ADF15 /* Info.plist */, 03E392BD2ADDA00F000ADF15 /* SingBoxPacketTunnel.entitlements */, ); @@ -240,7 +283,9 @@ 97C146F01CF9000F007C117D /* Runner */ = { isa = PBXGroup; children = ( + 03B5166E2AE7325D00EA47E2 /* Handlers */, 032158B62ADDF8AF008D943B /* VPN */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, 03E392C72ADDA26A000ADF15 /* Runner.entitlements */, 97C146FA1CF9000F007C117D /* Main.storyboard */, 97C146FD1CF9000F007C117D /* Assets.xcassets */, @@ -248,7 +293,6 @@ 97C147021CF9000F007C117D /* Info.plist */, 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, - 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, ); path = Runner; @@ -506,6 +550,7 @@ 03E392BB2ADDA00F000ADF15 /* PacketTunnelProvider.swift in Sources */, 03E392CF2ADDEFC8000ADF15 /* FilePath.swift in Sources */, 03E392D42ADDF262000ADF15 /* Extension+RunBlocking.swift in Sources */, + 03B516762AE762F700EA47E2 /* Logger.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -521,10 +566,19 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 03B516742AE74D2200EA47E2 /* Stored.swift in Sources */, + 03B5167B2AE79DB400EA47E2 /* FileMethodHandler.swift in Sources */, + 03B516772AE7634400EA47E2 /* Logger.swift in Sources */, 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 03B516712AE74CCD00EA47E2 /* VPNConfig.swift in Sources */, + 03B5166B2AE7315E00EA47E2 /* AlertsEventHandler.swift in Sources */, + 03B516692AE7306B00EA47E2 /* StatusEventHandler.swift in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, 032158B82ADDF8BF008D943B /* VPNManager.swift in Sources */, + 03B516672AE6B93A00EA47E2 /* MethodHandler.swift in Sources */, + 03B5166D2AE7325500EA47E2 /* LogsEventHandler.swift in Sources */, 03E392D02ADDF1BD000ADF15 /* FilePath.swift in Sources */, + 03B5167D2AE7AC6200EA47E2 /* GroupsEventHandler.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index 70693e4a..ac1a4e09 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -1,13 +1,35 @@ import UIKit import Flutter +import Libcore @UIApplicationMain @objc class AppDelegate: FlutterAppDelegate { - override func application( - _ application: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? - ) -> Bool { - GeneratedPluginRegistrant.register(with: self) - return super.application(application, didFinishLaunchingWithOptions: launchOptions) - } + + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + + setupFileManager() + + registerHandlers() + GeneratedPluginRegistrant.register(with: self) + + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } + + func setupFileManager() { + try? FileManager.default.createDirectory(at: FilePath.workingDirectory, withIntermediateDirectories: true) + FileManager.default.changeCurrentDirectoryPath(FilePath.sharedDirectory.path) + } + + func registerHandlers() { + MethodHandler.register(with: self.registrar(forPlugin: MethodHandler.name)!) + FileMethodHandler.register(with: self.registrar(forPlugin: FileMethodHandler.name)!) + StatusEventHandler.register(with: self.registrar(forPlugin: StatusEventHandler.name)!) + AlertsEventHandler.register(with: self.registrar(forPlugin: AlertsEventHandler.name)!) + LogsEventHandler.register(with: self.registrar(forPlugin: LogsEventHandler.name)!) + GroupsEventHandler.register(with: self.registrar(forPlugin: GroupsEventHandler.name)!) + } } + diff --git a/ios/Runner/Handlers/AlertsEventHandler.swift b/ios/Runner/Handlers/AlertsEventHandler.swift new file mode 100644 index 00000000..02551c9b --- /dev/null +++ b/ios/Runner/Handlers/AlertsEventHandler.swift @@ -0,0 +1,45 @@ +// +// AlertEventHandler.swift +// Runner +// +// Created by GFWFighter on 10/24/23. +// + +import Foundation +import Combine + +public class AlertsEventHandler: NSObject, FlutterPlugin, FlutterStreamHandler { + static let name = "\(FilePath.packageName)/service.alerts" + + private var channel: FlutterEventChannel? + + private var cancellable: AnyCancellable? + + public static func register(with registrar: FlutterPluginRegistrar) { + let instance = AlertsEventHandler() + instance.channel = FlutterEventChannel(name: Self.name, binaryMessenger: registrar.messenger()) + instance.channel?.setStreamHandler(instance) + } + + public func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { + cancellable = VPNManager.shared.$alert.sink { [events] alert in + var data = [ + "status": "Stopped", + "alert": alert.alert?.rawValue, + "message": alert.message, + ] + for key in data.keys { + if data[key] == nil { + data.removeValue(forKey: key) + } + } + events(data) + } + return nil + } + + public func onCancel(withArguments arguments: Any?) -> FlutterError? { + cancellable?.cancel() + return nil + } +} diff --git a/ios/Runner/Handlers/FileMethodHandler.swift b/ios/Runner/Handlers/FileMethodHandler.swift new file mode 100644 index 00000000..ca230f10 --- /dev/null +++ b/ios/Runner/Handlers/FileMethodHandler.swift @@ -0,0 +1,35 @@ +// +// FileMethodHandler.swift +// Runner +// +// Created by GFWFighter on 10/24/23. +// + +import Foundation + +public class FileMethodHandler: NSObject, FlutterPlugin { + + public static let name = "\(FilePath.packageName)/files.method" + + public static func register(with registrar: FlutterPluginRegistrar) { + let channel = FlutterMethodChannel(name: Self.name, binaryMessenger: registrar.messenger()) + let instance = FileMethodHandler() + registrar.addMethodCallDelegate(instance, channel: channel) + instance.channel = channel + } + + private var channel: FlutterMethodChannel? + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + switch call.method { + case "get_paths": + result([ + "working": FilePath.workingDirectory.path, + "temp": FilePath.cacheDirectory.path, + "base": FilePath.sharedDirectory.path + ]) + default: + result(FlutterMethodNotImplemented) + } + } +} diff --git a/ios/Runner/Handlers/GroupsEventHandler.swift b/ios/Runner/Handlers/GroupsEventHandler.swift new file mode 100644 index 00000000..4b4901c3 --- /dev/null +++ b/ios/Runner/Handlers/GroupsEventHandler.swift @@ -0,0 +1,93 @@ +// +// GroupsEventHandler.swift +// Runner +// +// Created by GFWFighter on 10/24/23. +// + +import Foundation +import Libcore + +struct SBItem: Codable { + let tag: String + let type: String + let urlTestDelay: Int + + enum CodingKeys: String, CodingKey { + case tag + case type + case urlTestDelay = "url-test-delay" + } +} + +struct SBGroup: Codable { + let tag: String + let type: String + let selected: String + let items: [SBItem] +} + +public class GroupsEventHandler: NSObject, FlutterPlugin, FlutterStreamHandler, LibboxCommandClientHandlerProtocol { + + static let name = "\(FilePath.packageName)/groups" + + private var channel: FlutterEventChannel? + + var commandClient: LibboxCommandClient? + var events: FlutterEventSink? + + public static func register(with registrar: FlutterPluginRegistrar) { + let instance = GroupsEventHandler() + instance.channel = FlutterEventChannel(name: Self.name, binaryMessenger: registrar.messenger()) + instance.channel?.setStreamHandler(instance) + } + + public func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { + FileManager.default.changeCurrentDirectoryPath(FilePath.sharedDirectory.path) + self.events = events + let opts = LibboxCommandClientOptions() + opts.command = LibboxCommandGroup + opts.statusInterval = 3000 + commandClient = LibboxCommandClient(self, options: opts) + try? commandClient?.connect() + return nil + } + + public func onCancel(withArguments arguments: Any?) -> FlutterError? { + try? commandClient?.disconnect() + return nil + } + + public func writeGroups(_ message: LibboxOutboundGroupIteratorProtocol?) { + guard let message else { return } + var groups = [SBGroup]() + while message.hasNext() { + let group = message.next()! + var items = [SBItem]() + var groupItems = group.getItems() + while groupItems?.hasNext() ?? false { + let item = groupItems?.next()! + items.append(SBItem(tag: item!.tag, type: item!.type, urlTestDelay: Int(item!.urlTestDelay))) + } + groups.append(.init(tag: group.tag, type: group.type, selected: group.selected, items: items)) + } + if + let groups = try? JSONEncoder().encode(groups), + let groups = String(data: groups, encoding: .utf8) + { + DispatchQueue.main.async { [events = self.events, groups] () in + events?(groups) + } + } + } +} + +extension GroupsEventHandler { + public func clearLog() {} + public func connected() {} + public func disconnected(_ message: String?) {} + public func initializeClashMode(_ modeList: LibboxStringIteratorProtocol?, currentMode: String?) {} + public func updateClashMode(_ newMode: String?) {} + public func writeLog(_ message: String?) {} + public func writeStatus(_ message: LibboxStatusMessage?) {} +} diff --git a/ios/Runner/Handlers/LogsEventHandler.swift b/ios/Runner/Handlers/LogsEventHandler.swift new file mode 100644 index 00000000..310234c8 --- /dev/null +++ b/ios/Runner/Handlers/LogsEventHandler.swift @@ -0,0 +1,28 @@ +// +// LogsEventHandler.swift +// Runner +// +// Created by GFWFighter on 10/24/23. +// + +import Foundation + +public class LogsEventHandler: NSObject, FlutterPlugin, FlutterStreamHandler { + static let name = "\(FilePath.packageName)/service.logs" + + private var channel: FlutterEventChannel? + + public static func register(with registrar: FlutterPluginRegistrar) { + let instance = LogsEventHandler() + instance.channel = FlutterEventChannel(name: Self.name, binaryMessenger: registrar.messenger()) + instance.channel?.setStreamHandler(instance) + } + + public func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { + return nil + } + + public func onCancel(withArguments arguments: Any?) -> FlutterError? { + return nil + } +} diff --git a/ios/Runner/Handlers/MethodHandler.swift b/ios/Runner/Handlers/MethodHandler.swift new file mode 100644 index 00000000..1c413b1e --- /dev/null +++ b/ios/Runner/Handlers/MethodHandler.swift @@ -0,0 +1,180 @@ +// +// MethodHandler.swift +// Runner +// +// Created by GFWFighter on 10/23/23. +// + +import Flutter +import Combine +import Libcore + +public class MethodHandler: NSObject, FlutterPlugin { + + private var cancelBag: Set = [] + + public static let name = "\(FilePath.packageName)/method" + + public static func register(with registrar: FlutterPluginRegistrar) { + let channel = FlutterMethodChannel(name: Self.name, binaryMessenger: registrar.messenger()) + let instance = MethodHandler() + registrar.addMethodCallDelegate(instance, channel: channel) + instance.channel = channel + } + + private var channel: FlutterMethodChannel? + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + switch call.method { + case "parse_config": + result(parseConfig(args: call.arguments)) + case "change_config_options": + result(changeConfigOptions(args: call.arguments)) + case "start": + Task { [unowned self] in + let res = await start(args: call.arguments) + await MainActor.run { + result(res) + } + } + case "restart": + Task { [unowned self] in + let res = await restart(args: call.arguments) + await MainActor.run { + result(res) + } + } + case "stop": + result(stop()) + case "url_test": + result(urlTest(args: call.arguments)) + case "select_outbound": + result(selectOutbound(args: call.arguments)) + default: + result(FlutterMethodNotImplemented) + } + } + + public func parseConfig(args: Any?) -> String { + var error: NSError? + guard + let args = args as? [String:Any?], + let path = args["path"] as? String, + let tempPath = args["tempPath"] as? String, + let debug = (args["debug"] as? NSNumber)?.boolValue + else { + return "bad method format" + } + let res = MobileParse(path, tempPath, debug, &error) + if let error { + return error.localizedDescription + } + return "" + } + + public func changeConfigOptions(args: Any?) -> Bool { + guard let options = args as? String else { + return false + } + VPNConfig.shared.configOptions = options + return true + } + + public func start(args: Any?) async -> Bool { + guard + let args = args as? [String:Any?], + let path = args["path"] as? String + else { + return false + } + VPNConfig.shared.activeConfigPath = path + var error: NSError? + let config = MobileBuildConfig(path, VPNConfig.shared.configOptions, &error) + if let error { + return false + } + do { + try await VPNManager.shared.setup() + try await VPNManager.shared.connect(with: config, disableMemoryLimit: VPNConfig.shared.disableMemoryLimit) + } catch { + return false + } + return true + } + + public func stop() -> Bool { + VPNManager.shared.disconnect() + return true + } + + private func waitForStop() -> Future { + return Future { promise in + var cancellable: AnyCancellable? = nil + cancellable = VPNManager.shared.$state + .filter { $0 == .disconnected } + .first() + .delay(for: 0.5, scheduler: RunLoop.current) + .sink(receiveValue: { _ in + promise(.success(())) + cancellable?.cancel() + }) + } + } + + public func restart(args: Any?) async -> Bool { + guard + let args = args as? [String:Any?], + let path = args["path"] as? String + else { + return false + } + VPNConfig.shared.activeConfigPath = path + VPNManager.shared.disconnect() + await waitForStop().value + var error: NSError? + let config = MobileBuildConfig(path, VPNConfig.shared.configOptions, &error) + if let error { + return false + } + do { + try await VPNManager.shared.setup() + try await VPNManager.shared.connect(with: config, disableMemoryLimit: VPNConfig.shared.disableMemoryLimit) + } catch { + return false + } + return true + } + + public func selectOutbound(args: Any?) -> Bool { + guard + let args = args as? [String:Any?], + let group = args["groupTag"] as? String, + let outbound = args["outboundTag"] as? String + else { + return false + } + FileManager.default.changeCurrentDirectoryPath(FilePath.sharedDirectory.path) + do { + try LibboxNewStandaloneCommandClient()?.selectOutbound(group, outboundTag: outbound) + } catch { + return false + } + return true + } + + public func urlTest(args: Any?) -> Bool { + guard + let args = args as? [String:Any?] + else { + return false + } + let group = args["groupTag"] as? String + FileManager.default.changeCurrentDirectoryPath(FilePath.sharedDirectory.path) + do { + try LibboxNewStandaloneCommandClient()?.urlTest(group) + } catch { + return false + } + return true + } +} diff --git a/ios/Runner/Handlers/StatusEventHandler.swift b/ios/Runner/Handlers/StatusEventHandler.swift new file mode 100644 index 00000000..26a4f2bb --- /dev/null +++ b/ios/Runner/Handlers/StatusEventHandler.swift @@ -0,0 +1,46 @@ +// +// StatusEventHandler.swift +// Runner +// +// Created by GFWFighter on 10/24/23. +// + +import Foundation +import Combine + +public class StatusEventHandler: NSObject, FlutterPlugin, FlutterStreamHandler { + static let name = "\(FilePath.packageName)/service.status" + + private var channel: FlutterEventChannel? + + private var cancellable: AnyCancellable? + + public static func register(with registrar: FlutterPluginRegistrar) { + let instance = StatusEventHandler() + instance.channel = FlutterEventChannel(name: Self.name, binaryMessenger: registrar.messenger()) + instance.channel?.setStreamHandler(instance) + } + + public func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { + cancellable = VPNManager.shared.$state.sink { [events] status in + switch status { + case .reasserting, .connecting: + events(["status": "Starting"]) + case .connected: + events(["status": "Started"]) + case .disconnecting: + events(["status": "Stopping"]) + case .disconnected, .invalid: + events(["status": "Stopped"]) + @unknown default: + events(["status": "Stopped"]) + } + } + return nil + } + + public func onCancel(withArguments arguments: Any?) -> FlutterError? { + cancellable?.cancel() + return nil + } +} diff --git a/ios/Runner/VPN/Helpers/Stored.swift b/ios/Runner/VPN/Helpers/Stored.swift new file mode 100644 index 00000000..9d07c4f9 --- /dev/null +++ b/ios/Runner/VPN/Helpers/Stored.swift @@ -0,0 +1,86 @@ +// +// Stored.swift +// Runner +// +// Created by GFWFighter on 10/24/23. +// + +import Foundation +import Combine + +enum StoredLocation { + case standard + + func data(for key: String) -> Data? { + switch self { + case .standard: + return UserDefaults.standard.data(forKey: key) + } + } + + func set(_ value: Data, for key: String) { + switch self { + case .standard: + UserDefaults.standard.set(value, forKey: key) + } + } +} + + +@propertyWrapper +struct Stored { + let location: StoredLocation + let key: String + var wrappedValue: Value { + willSet { // Before modifying wrappedValue + publisher.subject.send(newValue) + guard let value = try? JSONEncoder().encode(newValue) else { + return + } + location.set(value, for: key) + } + } + + var projectedValue: Publisher { + publisher + } + private var publisher: Publisher + struct Publisher: Combine.Publisher { + typealias Output = Value + typealias Failure = Never + var subject: CurrentValueSubject // PassthroughSubject will lack the call of initial assignment + func receive(subscriber: S) where S: Subscriber, Self.Failure == S.Failure, Self.Output == S.Input { + subject.subscribe(subscriber) + } + init(_ output: Output) { + subject = .init(output) + } + } + init(wrappedValue: Value, key: String, in location: StoredLocation = .standard) { + self.location = location + self.key = key + var value = wrappedValue + if let data = location.data(for: key) { + do { + value = try JSONDecoder().decode(Value.self, from: data) + } catch {} + } + self.wrappedValue = value + publisher = Publisher(value) + } + static subscript( + _enclosingInstance observed: OuterSelf, + wrapped wrappedKeyPath: ReferenceWritableKeyPath, + storage storageKeyPath: ReferenceWritableKeyPath + ) -> Value { + get { + observed[keyPath: storageKeyPath].wrappedValue + } + set { + if let subject = observed.objectWillChange as? ObservableObjectPublisher { + subject.send() // Before modifying wrappedValue + observed[keyPath: storageKeyPath].wrappedValue = newValue + } + } + } +} diff --git a/ios/Runner/VPN/VPNConfig.swift b/ios/Runner/VPN/VPNConfig.swift new file mode 100644 index 00000000..f8e7a536 --- /dev/null +++ b/ios/Runner/VPN/VPNConfig.swift @@ -0,0 +1,22 @@ +// +// VPNConfig.swift +// Runner +// +// Created by GFWFighter on 10/24/23. +// + +import Foundation +import Combine + +class VPNConfig: ObservableObject { + static let shared = VPNConfig() + + @Stored(key: "VPN.ActiveConfigPath") + var activeConfigPath: String = "" + + @Stored(key: "VPN.ConfigOptions") + var configOptions: String = "" + + @Stored(key: "VPN.DisableMemoryLimit") + var disableMemoryLimit: Bool = false +} diff --git a/ios/Runner/VPN/VPNManager.swift b/ios/Runner/VPN/VPNManager.swift index 8b69adf2..4af31f0b 100644 --- a/ios/Runner/VPN/VPNManager.swift +++ b/ios/Runner/VPN/VPNManager.swift @@ -9,6 +9,20 @@ import Foundation import Combine import NetworkExtension +enum VPNManagerAlertType: String { + case RequestVPNPermission + case RequestNotificationPermission + case EmptyConfiguration + case StartCommandServer + case CreateService + case StartService +} + +struct VPNManagerAlert { + let alert: VPNManagerAlertType? + let message: String? +} + class VPNManager: ObservableObject { private var cancelBag: Set = [] @@ -20,6 +34,7 @@ class VPNManager: ObservableObject { static let shared: VPNManager = VPNManager() @Published private(set) var state: NEVPNStatus = .invalid + @Published private(set) var alert: VPNManagerAlert = .init(alert: nil, message: nil) @Published private(set) var upload: Int64 = 0 @Published private(set) var download: Int64 = 0 diff --git a/ios/Shared/FilePath.swift b/ios/Shared/FilePath.swift index a3307cea..762cf1c2 100644 --- a/ios/Shared/FilePath.swift +++ b/ios/Shared/FilePath.swift @@ -23,8 +23,6 @@ public extension FilePath { static let cacheDirectory = sharedDirectory .appendingPathComponent("Library", isDirectory: true) .appendingPathComponent("Caches", isDirectory: true) - - static let assetsDirectory = cacheDirectory.appendingPathComponent("Assets", isDirectory: true) static let workingDirectory = cacheDirectory.appendingPathComponent("Working", isDirectory: true) } diff --git a/ios/SingBoxPacketTunnel/Logger.swift b/ios/SingBoxPacketTunnel/Logger.swift new file mode 100644 index 00000000..99edbd5f --- /dev/null +++ b/ios/SingBoxPacketTunnel/Logger.swift @@ -0,0 +1,52 @@ +// +// Logger.swift +// SingBoxPacketTunnel +// +// Created by GFWFighter on 10/24/23. +// + +import Foundation + +class Logger { + private static let queue = DispatchQueue.init(label: "\(FilePath.packageName).PacketTunnelLog", qos: .utility) + + private let fileManager = FileManager.default + private let url: URL + + private var _fileHandle: FileHandle? + private var fileHandle: FileHandle? { + get { + if let _fileHandle { return _fileHandle } + let handle = try? FileHandle(forWritingTo: url) + _fileHandle = handle + return handle + } + } + + private var lock = NSLock() + + init(path: URL) { + url = path + } + + func write(_ message: String) { + Logger.queue.async { [message, unowned self] () in + lock.lock() + defer { lock.unlock() } + let output = message + "\n" + do { + if !self.fileManager.fileExists(atPath: url.path) { + try output.write(to: url, atomically: true, encoding: .utf8) + } else { + guard let fileHandle else { + return + } + fileHandle.seekToEndOfFile() + if let data = output.data(using: .utf8) { + fileHandle.write(data) + } + } + } catch {} + } + } +} diff --git a/ios/SingBoxPacketTunnel/PacketTunnelProvider.swift b/ios/SingBoxPacketTunnel/PacketTunnelProvider.swift index fdff8281..093e2ddb 100644 --- a/ios/SingBoxPacketTunnel/PacketTunnelProvider.swift +++ b/ios/SingBoxPacketTunnel/PacketTunnelProvider.swift @@ -11,18 +11,18 @@ class PacketTunnelProvider: ExtensionProvider { private var upload: Int64 = 0 private var download: Int64 = 0 - private var trafficLock: NSLock = NSLock() + // private var trafficLock: NSLock = NSLock() - var trafficReader: TrafficReader! + // var trafficReader: TrafficReader! override func startTunnel(options: [String : NSObject]?) async throws { try await super.startTunnel(options: options) - trafficReader = TrafficReader { [unowned self] traffic in + /*trafficReader = TrafficReader { [unowned self] traffic in trafficLock.lock() upload += traffic.up download += traffic.down trafficLock.unlock() - } + }*/ } override func handleAppMessage(_ messageData: Data) async -> Data? { diff --git a/ios/SingBoxPacketTunnel/SingBox/ExtensionProvider.swift b/ios/SingBoxPacketTunnel/SingBox/ExtensionProvider.swift index e07177a0..b0f1d95b 100644 --- a/ios/SingBoxPacketTunnel/SingBox/ExtensionProvider.swift +++ b/ios/SingBoxPacketTunnel/SingBox/ExtensionProvider.swift @@ -18,8 +18,11 @@ open class ExtensionProvider: NEPacketTunnelProvider { private var systemProxyEnabled = false private var platformInterface: ExtensionPlatformInterface! private var config: String! - + override open func startTunnel(options: [String: NSObject]?) async throws { + try? FileManager.default.removeItem(at: ExtensionProvider.errorFile) + try? FileManager.default.removeItem(at: FilePath.workingDirectory.appendingPathComponent("TestLog")) + let disableMemoryLimit = (options?["DisableMemoryLimit"] as? NSString as? String ?? "NO") == "YES" guard let config = options?["Config"] as? NSString as? String else { @@ -31,8 +34,6 @@ open class ExtensionProvider: NEPacketTunnelProvider { return } self.config = config - - try? FileManager.default.removeItem(at: ExtensionProvider.errorFile) do { try FileManager.default.createDirectory(at: FilePath.workingDirectory, withIntermediateDirectories: true) @@ -41,7 +42,12 @@ open class ExtensionProvider: NEPacketTunnelProvider { return } - LibboxSetup(FilePath.sharedDirectory.relativePath, FilePath.workingDirectory.relativePath, FilePath.cacheDirectory.relativePath, false) + LibboxSetup( + FilePath.sharedDirectory.relativePath, + FilePath.workingDirectory.relativePath, + FilePath.cacheDirectory.relativePath, + false + ) var error: NSError? LibboxRedirectStderr(FilePath.cacheDirectory.appendingPathComponent("stderr.log").relativePath, &error) diff --git a/ios/SingBoxPacketTunnel/SingBox/SingBox.swift b/ios/SingBoxPacketTunnel/SingBox/SingBox.swift index 02fb07a2..c3911c24 100644 --- a/ios/SingBoxPacketTunnel/SingBox/SingBox.swift +++ b/ios/SingBoxPacketTunnel/SingBox/SingBox.swift @@ -19,7 +19,7 @@ class SingBox { else { return nil } - json["log"] = [ + /*json["log"] = [ "disabled": false, "level": "info", "output": "log", @@ -50,7 +50,7 @@ class SingBox { routing["geosite"] = [ "path": FilePath.assetsDirectory.appendingPathComponent("geosite.db"), ] - json["route"] = routing + json["route"] = routing*/ guard let data = try? JSONSerialization.data(withJSONObject: json) else { return nil } diff --git a/lib/services/files_editor_service.dart b/lib/services/files_editor_service.dart index aba670fd..3bba89db 100644 --- a/lib/services/files_editor_service.dart +++ b/lib/services/files_editor_service.dart @@ -8,23 +8,43 @@ import 'package:path/path.dart' as p; import 'package:path_provider/path_provider.dart'; class FilesEditorService with InfraLogger { + + late final _methodChannel = const MethodChannel("com.hiddify.app/files.method"); + late final Directory baseDir; late final Directory workingDir; late final Directory tempDir; late final Directory logsDir; late final Directory _configsDir; - Future init() async { - baseDir = await getApplicationSupportDirectory(); - if (Platform.isAndroid) { - final externalDir = await getExternalStorageDirectory(); - workingDir = externalDir!; - } else if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) { - workingDir = baseDir; - } else { - workingDir = await getApplicationDocumentsDirectory(); + Future?> getPaths() async { + try { + final Map? directoryMap = await _methodChannel.invokeMethod('get_paths'); + return directoryMap?.cast(); + } on PlatformException catch (e) { + // print("Failed to get shared directory: '${e.message}'."); + return null; + } + } + + Future init() async { + if (Platform.isIOS) { + final paths = await getPaths(); + baseDir = Directory(paths!["base"]!); + workingDir = Directory(paths["working"]!); + tempDir = Directory(paths["temp"]!); + } else { + baseDir = await getApplicationSupportDirectory(); + if (Platform.isAndroid) { + final externalDir = await getExternalStorageDirectory(); + workingDir = externalDir!; + } else if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) { + workingDir = baseDir; + } else { + workingDir = await getApplicationDocumentsDirectory(); + } + tempDir = await getTemporaryDirectory(); } - tempDir = await getTemporaryDirectory(); logsDir = workingDir; loggy.debug("base dir: ${baseDir.path}"); diff --git a/lib/services/singbox/singbox_service.dart b/lib/services/singbox/singbox_service.dart index 5f1c31f4..e078c849 100644 --- a/lib/services/singbox/singbox_service.dart +++ b/lib/services/singbox/singbox_service.dart @@ -8,7 +8,7 @@ import 'package:hiddify/services/singbox/mobile_singbox_service.dart'; abstract interface class SingboxService { factory SingboxService() { - if (Platform.isAndroid) { + if (Platform.isAndroid || Platform.isIOS) { return MobileSingboxService(); } else if (Platform.isLinux || Platform.isWindows || Platform.isMacOS) { return FFISingboxService();