First working version
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 = "<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>"; };
|
||||
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; };
|
||||
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>"; };
|
||||
@@ -144,17 +163,41 @@
|
||||
032158B62ADDF8AF008D943B /* VPN */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
03B516722AE74D1700EA47E2 /* Helpers */,
|
||||
032158B72ADDF8BF008D943B /* VPNManager.swift */,
|
||||
03B516702AE74CCD00EA47E2 /* VPNConfig.swift */,
|
||||
);
|
||||
path = VPN;
|
||||
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 */ = {
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -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)!)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
45
ios/Runner/Handlers/AlertsEventHandler.swift
Normal file
45
ios/Runner/Handlers/AlertsEventHandler.swift
Normal 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
|
||||
}
|
||||
}
|
||||
35
ios/Runner/Handlers/FileMethodHandler.swift
Normal file
35
ios/Runner/Handlers/FileMethodHandler.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
93
ios/Runner/Handlers/GroupsEventHandler.swift
Normal file
93
ios/Runner/Handlers/GroupsEventHandler.swift
Normal 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?) {}
|
||||
}
|
||||
28
ios/Runner/Handlers/LogsEventHandler.swift
Normal file
28
ios/Runner/Handlers/LogsEventHandler.swift
Normal 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
|
||||
}
|
||||
}
|
||||
180
ios/Runner/Handlers/MethodHandler.swift
Normal file
180
ios/Runner/Handlers/MethodHandler.swift
Normal 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
|
||||
}
|
||||
}
|
||||
46
ios/Runner/Handlers/StatusEventHandler.swift
Normal file
46
ios/Runner/Handlers/StatusEventHandler.swift
Normal 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
|
||||
}
|
||||
}
|
||||
86
ios/Runner/VPN/Helpers/Stored.swift
Normal file
86
ios/Runner/VPN/Helpers/Stored.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
22
ios/Runner/VPN/VPNConfig.swift
Normal file
22
ios/Runner/VPN/VPNConfig.swift
Normal 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
|
||||
}
|
||||
@@ -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<AnyCancellable> = []
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
52
ios/SingBoxPacketTunnel/Logger.swift
Normal file
52
ios/SingBoxPacketTunnel/Logger.swift
Normal 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 {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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? {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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<void> 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<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 {
|
||||
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}");
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user