First working version
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user