First working version

This commit is contained in:
GFWFighter
2023-10-24 18:29:53 +03:30
parent fe7cdc276e
commit b83fcce310
19 changed files with 734 additions and 32 deletions

View File

@@ -5,5 +5,5 @@
// Created by GFWFighter on 7/24/1402 AP. // Created by GFWFighter on 7/24/1402 AP.
// //
BASE_BUNDLE_IDENTIFIER=com.hiddify.next BASE_BUNDLE_IDENTIFIER=com.hiddify.app
DEVELOPMENT_TEAM=XXXXXXXXX DEVELOPMENT_TEAM=XXXXXXXXX

View File

@@ -10,6 +10,16 @@
032158B82ADDF8BF008D943B /* VPNManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 032158B72ADDF8BF008D943B /* VPNManager.swift */; }; 032158B82ADDF8BF008D943B /* VPNManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 032158B72ADDF8BF008D943B /* VPNManager.swift */; };
032158BA2ADDFCC9008D943B /* TrafficReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 032158B92ADDFCC9008D943B /* TrafficReader.swift */; }; 032158BA2ADDFCC9008D943B /* TrafficReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 032158B92ADDFCC9008D943B /* TrafficReader.swift */; };
032158BC2ADDFD09008D943B /* SingBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 032158BB2ADDFD09008D943B /* SingBox.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 */; }; 03E392B82ADDA00E000ADF15 /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 03E392B72ADDA00E000ADF15 /* NetworkExtension.framework */; };
03E392BB2ADDA00F000ADF15 /* PacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03E392BA2ADDA00F000ADF15 /* PacketTunnelProvider.swift */; }; 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, ); }; }; 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 = "<group>"; }; 032158B72ADDF8BF008D943B /* VPNManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNManager.swift; sourceTree = "<group>"; };
032158B92ADDFCC9008D943B /* TrafficReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrafficReader.swift; sourceTree = "<group>"; }; 032158B92ADDFCC9008D943B /* TrafficReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrafficReader.swift; sourceTree = "<group>"; };
032158BB2ADDFD09008D943B /* SingBox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingBox.swift; sourceTree = "<group>"; }; 032158BB2ADDFD09008D943B /* SingBox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingBox.swift; sourceTree = "<group>"; };
03B516662AE6B93A00EA47E2 /* MethodHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MethodHandler.swift; sourceTree = "<group>"; };
03B516682AE7306B00EA47E2 /* StatusEventHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusEventHandler.swift; sourceTree = "<group>"; };
03B5166A2AE7315E00EA47E2 /* AlertsEventHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertsEventHandler.swift; sourceTree = "<group>"; };
03B5166C2AE7325500EA47E2 /* LogsEventHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogsEventHandler.swift; sourceTree = "<group>"; };
03B516702AE74CCD00EA47E2 /* VPNConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNConfig.swift; sourceTree = "<group>"; };
03B516732AE74D2200EA47E2 /* Stored.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Stored.swift; sourceTree = "<group>"; };
03B516752AE762F700EA47E2 /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = "<group>"; };
03B5167A2AE79DB400EA47E2 /* FileMethodHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileMethodHandler.swift; sourceTree = "<group>"; };
03B5167C2AE7AC6200EA47E2 /* GroupsEventHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupsEventHandler.swift; sourceTree = "<group>"; };
03E392B62ADDA00E000ADF15 /* SingBoxPacketTunnel.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = SingBoxPacketTunnel.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 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; }; 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 = "<group>"; }; 03E392BA2ADDA00F000ADF15 /* PacketTunnelProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelProvider.swift; sourceTree = "<group>"; };
@@ -144,17 +163,41 @@
032158B62ADDF8AF008D943B /* VPN */ = { 032158B62ADDF8AF008D943B /* VPN */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
03B516722AE74D1700EA47E2 /* Helpers */,
032158B72ADDF8BF008D943B /* VPNManager.swift */, 032158B72ADDF8BF008D943B /* VPNManager.swift */,
03B516702AE74CCD00EA47E2 /* VPNConfig.swift */,
); );
path = VPN; path = VPN;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
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 = "<group>";
};
03B516722AE74D1700EA47E2 /* Helpers */ = {
isa = PBXGroup;
children = (
03B516732AE74D2200EA47E2 /* Stored.swift */,
);
path = Helpers;
sourceTree = "<group>";
};
03E392B92ADDA00F000ADF15 /* SingBoxPacketTunnel */ = { 03E392B92ADDA00F000ADF15 /* SingBoxPacketTunnel */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
03E392CA2ADDE063000ADF15 /* SingBox */, 03E392CA2ADDE063000ADF15 /* SingBox */,
03E392BA2ADDA00F000ADF15 /* PacketTunnelProvider.swift */, 03E392BA2ADDA00F000ADF15 /* PacketTunnelProvider.swift */,
032158B92ADDFCC9008D943B /* TrafficReader.swift */, 032158B92ADDFCC9008D943B /* TrafficReader.swift */,
03B516752AE762F700EA47E2 /* Logger.swift */,
03E392BC2ADDA00F000ADF15 /* Info.plist */, 03E392BC2ADDA00F000ADF15 /* Info.plist */,
03E392BD2ADDA00F000ADF15 /* SingBoxPacketTunnel.entitlements */, 03E392BD2ADDA00F000ADF15 /* SingBoxPacketTunnel.entitlements */,
); );
@@ -240,7 +283,9 @@
97C146F01CF9000F007C117D /* Runner */ = { 97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
03B5166E2AE7325D00EA47E2 /* Handlers */,
032158B62ADDF8AF008D943B /* VPN */, 032158B62ADDF8AF008D943B /* VPN */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
03E392C72ADDA26A000ADF15 /* Runner.entitlements */, 03E392C72ADDA26A000ADF15 /* Runner.entitlements */,
97C146FA1CF9000F007C117D /* Main.storyboard */, 97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */, 97C146FD1CF9000F007C117D /* Assets.xcassets */,
@@ -248,7 +293,6 @@
97C147021CF9000F007C117D /* Info.plist */, 97C147021CF9000F007C117D /* Info.plist */,
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
); );
path = Runner; path = Runner;
@@ -506,6 +550,7 @@
03E392BB2ADDA00F000ADF15 /* PacketTunnelProvider.swift in Sources */, 03E392BB2ADDA00F000ADF15 /* PacketTunnelProvider.swift in Sources */,
03E392CF2ADDEFC8000ADF15 /* FilePath.swift in Sources */, 03E392CF2ADDEFC8000ADF15 /* FilePath.swift in Sources */,
03E392D42ADDF262000ADF15 /* Extension+RunBlocking.swift in Sources */, 03E392D42ADDF262000ADF15 /* Extension+RunBlocking.swift in Sources */,
03B516762AE762F700EA47E2 /* Logger.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@@ -521,10 +566,19 @@
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
03B516742AE74D2200EA47E2 /* Stored.swift in Sources */,
03B5167B2AE79DB400EA47E2 /* FileMethodHandler.swift in Sources */,
03B516772AE7634400EA47E2 /* Logger.swift in Sources */,
74858FAF1ED2DC5600515810 /* AppDelegate.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 */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
032158B82ADDF8BF008D943B /* VPNManager.swift in Sources */, 032158B82ADDF8BF008D943B /* VPNManager.swift in Sources */,
03B516672AE6B93A00EA47E2 /* MethodHandler.swift in Sources */,
03B5166D2AE7325500EA47E2 /* LogsEventHandler.swift in Sources */,
03E392D02ADDF1BD000ADF15 /* FilePath.swift in Sources */, 03E392D02ADDF1BD000ADF15 /* FilePath.swift in Sources */,
03B5167D2AE7AC6200EA47E2 /* GroupsEventHandler.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };

View File

@@ -1,13 +1,35 @@
import UIKit import UIKit
import Flutter import Flutter
import Libcore
@UIApplicationMain @UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate { @objc class AppDelegate: FlutterAppDelegate {
override func application( override func application(
_ application: UIApplication, _ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool { ) -> Bool {
setupFileManager()
registerHandlers()
GeneratedPluginRegistrant.register(with: self) GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions) 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)!)
}
} }

View File

@@ -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
}
}

View File

@@ -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)
}
}
}

View File

@@ -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?) {}
}

View File

@@ -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
}
}

View File

@@ -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<AnyCancellable> = []
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<Void, Never> {
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
}
}

View File

@@ -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
}
}

View File

@@ -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<Value: Codable> {
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<Value, Never> // PassthroughSubject will lack the call of initial assignment
func receive<S>(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<OuterSelf: ObservableObject>(
_enclosingInstance observed: OuterSelf,
wrapped wrappedKeyPath: ReferenceWritableKeyPath<OuterSelf, Value>,
storage storageKeyPath: ReferenceWritableKeyPath<OuterSelf, Self>
) -> Value {
get {
observed[keyPath: storageKeyPath].wrappedValue
}
set {
if let subject = observed.objectWillChange as? ObservableObjectPublisher {
subject.send() // Before modifying wrappedValue
observed[keyPath: storageKeyPath].wrappedValue = newValue
}
}
}
}

View File

@@ -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
}

View File

@@ -9,6 +9,20 @@ import Foundation
import Combine import Combine
import NetworkExtension 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 { class VPNManager: ObservableObject {
private var cancelBag: Set<AnyCancellable> = [] private var cancelBag: Set<AnyCancellable> = []
@@ -20,6 +34,7 @@ class VPNManager: ObservableObject {
static let shared: VPNManager = VPNManager() static let shared: VPNManager = VPNManager()
@Published private(set) var state: NEVPNStatus = .invalid @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 upload: Int64 = 0
@Published private(set) var download: Int64 = 0 @Published private(set) var download: Int64 = 0

View File

@@ -24,8 +24,6 @@ public extension FilePath {
.appendingPathComponent("Library", isDirectory: true) .appendingPathComponent("Library", isDirectory: true)
.appendingPathComponent("Caches", isDirectory: true) .appendingPathComponent("Caches", isDirectory: true)
static let assetsDirectory = cacheDirectory.appendingPathComponent("Assets", isDirectory: true)
static let workingDirectory = cacheDirectory.appendingPathComponent("Working", isDirectory: true) static let workingDirectory = cacheDirectory.appendingPathComponent("Working", isDirectory: true)
} }

View File

@@ -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 {}
}
}
}

View File

@@ -11,18 +11,18 @@ class PacketTunnelProvider: ExtensionProvider {
private var upload: Int64 = 0 private var upload: Int64 = 0
private var download: 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 { override func startTunnel(options: [String : NSObject]?) async throws {
try await super.startTunnel(options: options) try await super.startTunnel(options: options)
trafficReader = TrafficReader { [unowned self] traffic in /*trafficReader = TrafficReader { [unowned self] traffic in
trafficLock.lock() trafficLock.lock()
upload += traffic.up upload += traffic.up
download += traffic.down download += traffic.down
trafficLock.unlock() trafficLock.unlock()
} }*/
} }
override func handleAppMessage(_ messageData: Data) async -> Data? { override func handleAppMessage(_ messageData: Data) async -> Data? {

View File

@@ -20,6 +20,9 @@ open class ExtensionProvider: NEPacketTunnelProvider {
private var config: String! private var config: String!
override open func startTunnel(options: [String: NSObject]?) async throws { 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" let disableMemoryLimit = (options?["DisableMemoryLimit"] as? NSString as? String ?? "NO") == "YES"
guard let config = options?["Config"] as? NSString as? String else { guard let config = options?["Config"] as? NSString as? String else {
@@ -32,8 +35,6 @@ open class ExtensionProvider: NEPacketTunnelProvider {
} }
self.config = config self.config = config
try? FileManager.default.removeItem(at: ExtensionProvider.errorFile)
do { do {
try FileManager.default.createDirectory(at: FilePath.workingDirectory, withIntermediateDirectories: true) try FileManager.default.createDirectory(at: FilePath.workingDirectory, withIntermediateDirectories: true)
} catch { } catch {
@@ -41,7 +42,12 @@ open class ExtensionProvider: NEPacketTunnelProvider {
return 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? var error: NSError?
LibboxRedirectStderr(FilePath.cacheDirectory.appendingPathComponent("stderr.log").relativePath, &error) LibboxRedirectStderr(FilePath.cacheDirectory.appendingPathComponent("stderr.log").relativePath, &error)

View File

@@ -19,7 +19,7 @@ class SingBox {
else { else {
return nil return nil
} }
json["log"] = [ /*json["log"] = [
"disabled": false, "disabled": false,
"level": "info", "level": "info",
"output": "log", "output": "log",
@@ -50,7 +50,7 @@ class SingBox {
routing["geosite"] = [ routing["geosite"] = [
"path": FilePath.assetsDirectory.appendingPathComponent("geosite.db"), "path": FilePath.assetsDirectory.appendingPathComponent("geosite.db"),
] ]
json["route"] = routing json["route"] = routing*/
guard let data = try? JSONSerialization.data(withJSONObject: json) else { guard let data = try? JSONSerialization.data(withJSONObject: json) else {
return nil return nil
} }

View File

@@ -8,13 +8,32 @@ import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
class FilesEditorService with InfraLogger { class FilesEditorService with InfraLogger {
late final _methodChannel = const MethodChannel("com.hiddify.app/files.method");
late final Directory baseDir; late final Directory baseDir;
late final Directory workingDir; late final Directory workingDir;
late final Directory tempDir; late final Directory tempDir;
late final Directory logsDir; late final Directory logsDir;
late final Directory _configsDir; late final Directory _configsDir;
Future<Map<String, String>?> getPaths() async {
try {
final Map<dynamic, dynamic>? directoryMap = await _methodChannel.invokeMethod('get_paths');
return directoryMap?.cast<String, String>();
} on PlatformException catch (e) {
// print("Failed to get shared directory: '${e.message}'.");
return null;
}
}
Future<void> init() async { Future<void> 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(); baseDir = await getApplicationSupportDirectory();
if (Platform.isAndroid) { if (Platform.isAndroid) {
final externalDir = await getExternalStorageDirectory(); final externalDir = await getExternalStorageDirectory();
@@ -25,6 +44,7 @@ class FilesEditorService with InfraLogger {
workingDir = await getApplicationDocumentsDirectory(); workingDir = await getApplicationDocumentsDirectory();
} }
tempDir = await getTemporaryDirectory(); tempDir = await getTemporaryDirectory();
}
logsDir = workingDir; logsDir = workingDir;
loggy.debug("base dir: ${baseDir.path}"); loggy.debug("base dir: ${baseDir.path}");

View File

@@ -8,7 +8,7 @@ import 'package:hiddify/services/singbox/mobile_singbox_service.dart';
abstract interface class SingboxService { abstract interface class SingboxService {
factory SingboxService() { factory SingboxService() {
if (Platform.isAndroid) { if (Platform.isAndroid || Platform.isIOS) {
return MobileSingboxService(); return MobileSingboxService();
} else if (Platform.isLinux || Platform.isWindows || Platform.isMacOS) { } else if (Platform.isLinux || Platform.isWindows || Platform.isMacOS) {
return FFISingboxService(); return FFISingboxService();