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

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

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