初始化提交

This commit is contained in:
2025-09-23 16:23:15 +08:00
commit 877b18a70f
590 changed files with 53617 additions and 0 deletions
+35
View File
@@ -0,0 +1,35 @@
import UIKit
import Flutter
import Libcore
@main
@objc class AppDelegate: FlutterAppDelegate {
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() {
KRMethodHandler.register(with: self.registrar(forPlugin: KRMethodHandler.name)!)
KRPlatformMethodHandler.register(with: self.registrar(forPlugin: KRPlatformMethodHandler.name)!)
KRFileMethodHandler.register(with: self.registrar(forPlugin: KRFileMethodHandler.name)!)
KRStatusEventHandler.register(with: self.registrar(forPlugin: KRStatusEventHandler.name)!)
KRAlertsEventHandler.register(with: self.registrar(forPlugin: KRAlertsEventHandler.name)!)
KRLogsEventHandler.register(with: self.registrar(forPlugin: KRLogsEventHandler.name)!)
KRGroupsEventHandler.register(with: self.registrar(forPlugin: KRGroupsEventHandler.name)!)
KRActiveGroupsEventHandler.register(with: self.registrar(forPlugin: KRActiveGroupsEventHandler.name)!)
KRStatsEventHandler.register(with: self.registrar(forPlugin: KRStatsEventHandler.name)!)
}
}
+120
View File
@@ -0,0 +1,120 @@
{
"images": [
{
"size": "20x20",
"idiom": "universal",
"filename": "icon-20@2x.png",
"scale": "2x",
"platform": "ios"
},
{
"size": "20x20",
"idiom": "universal",
"filename": "icon-20@3x.png",
"scale": "3x",
"platform": "ios"
},
{
"size": "29x29",
"idiom": "universal",
"filename": "icon-29@2x.png",
"scale": "2x",
"platform": "ios"
},
{
"size": "29x29",
"idiom": "universal",
"filename": "icon-29@3x.png",
"scale": "3x",
"platform": "ios"
},
{
"size": "38x38",
"idiom": "universal",
"filename": "icon-38@2x.png",
"scale": "2x",
"platform": "ios"
},
{
"size": "38x38",
"idiom": "universal",
"filename": "icon-38@3x.png",
"scale": "3x",
"platform": "ios"
},
{
"size": "40x40",
"idiom": "universal",
"filename": "icon-40@2x.png",
"scale": "2x",
"platform": "ios"
},
{
"size": "40x40",
"idiom": "universal",
"filename": "icon-40@3x.png",
"scale": "3x",
"platform": "ios"
},
{
"size": "60x60",
"idiom": "universal",
"filename": "icon-60@2x.png",
"scale": "2x",
"platform": "ios"
},
{
"size": "60x60",
"idiom": "universal",
"filename": "icon-60@3x.png",
"scale": "3x",
"platform": "ios"
},
{
"size": "64x64",
"idiom": "universal",
"filename": "icon-64@2x.png",
"scale": "2x",
"platform": "ios"
},
{
"size": "64x64",
"idiom": "universal",
"filename": "icon-64@3x.png",
"scale": "3x",
"platform": "ios"
},
{
"size": "68x68",
"idiom": "universal",
"filename": "icon-68@2x.png",
"scale": "2x",
"platform": "ios"
},
{
"size": "76x76",
"idiom": "universal",
"filename": "icon-76@2x.png",
"scale": "2x",
"platform": "ios"
},
{
"size": "83.5x83.5",
"idiom": "universal",
"filename": "icon-83.5@2x.png",
"scale": "2x",
"platform": "ios"
},
{
"size": "1024x1024",
"idiom": "universal",
"filename": "icon-1024.png",
"scale": "1x",
"platform": "ios"
}
],
"info": {
"version": 1,
"author": "icon.wuruihong.com"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

+6
View File
@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
+21
View File
@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "background.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 69 B

+24
View File
@@ -0,0 +1,24 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "icon-83.5@2x.png",
"scale" : "1x"
},
{
"filename" : "icon-83.5@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "icon-83.5@2x.png",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
+5
View File
@@ -0,0 +1,5 @@
# Launch Screen Assets
You can customize the launch screen with your own desired assets by replacing the image files in this directory.
You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

+44
View File
@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleToFill" image="LaunchBackground" translatesAutoresizingMaskIntoConstraints="NO" id="tWc-Dq-wcI"/>
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4"></imageView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="leading" secondItem="Ze5-6b-2t3" secondAttribute="leading" id="3T2-ad-Qdv"/>
<constraint firstItem="tWc-Dq-wcI" firstAttribute="bottom" secondItem="Ze5-6b-2t3" secondAttribute="bottom" id="RPx-PI-7Xg"/>
<constraint firstItem="tWc-Dq-wcI" firstAttribute="top" secondItem="Ze5-6b-2t3" secondAttribute="top" id="SdS-ul-q2q"/>
<constraint firstAttribute="trailing" secondItem="tWc-Dq-wcI" secondAttribute="trailing" id="Swv-Gf-Rwn"/>
<constraint firstAttribute="trailing" secondItem="YRO-k0-Ey4" secondAttribute="trailing" id="TQA-XW-tRk"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="bottom" secondItem="Ze5-6b-2t3" secondAttribute="bottom" id="duK-uY-Gun"/>
<constraint firstItem="tWc-Dq-wcI" firstAttribute="leading" secondItem="Ze5-6b-2t3" secondAttribute="leading" id="kV7-tw-vXt"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="top" secondItem="Ze5-6b-2t3" secondAttribute="top" id="xPn-NY-SIU"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
<resources>
<image name="LaunchImage" width="1024" height="1024"/>
<image name="LaunchBackground" width="1" height="1"/>
</resources>
</document>
+29
View File
@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="22154" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
<device id="retina6_12" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22130"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--Flutter View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-26" y="-77"/>
</scene>
</scenes>
</document>
+18
View File
@@ -0,0 +1,18 @@
//
// Bundle+Properties.swift
// Runner
//
// Created by Hiddify on 12/26/23.
//
import Foundation
extension Bundle {
var serviceIdentifier: String {
(infoDictionary?["SERVICE_IDENTIFIER"] as? String)!
}
var baseBundleIdentifier: String {
(infoDictionary?["BASE_BUNDLE_IDENTIFIER"] as? String)!
}
}
+50
View File
@@ -0,0 +1,50 @@
import Foundation
import Combine
import Libcore
public class KRActiveGroupsEventHandler: NSObject, FlutterPlugin, FlutterStreamHandler {
static let name = "\(Bundle.main.serviceIdentifier)/active-groups"
private var commandClient: CommandClient?
private var channel: FlutterEventChannel?
private var events: FlutterEventSink?
private var cancellable: AnyCancellable?
public static func register(with registrar: FlutterPluginRegistrar) {
let instance = KRActiveGroupsEventHandler()
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
commandClient = CommandClient(.groupsInfoOnly)
commandClient?.connect()
cancellable = commandClient?.$groups.sink{ [self] sbGroups in
self.kr_writeGroups(sbGroups)
}
return nil
}
public func onCancel(withArguments arguments: Any?) -> FlutterError? {
commandClient?.disconnect()
cancellable?.cancel()
events = nil
return nil
}
func kr_writeGroups(_ sbGroups: [SBGroup]?) {
guard let sbGroups else {return}
if
let groups = try? JSONEncoder().encode(sbGroups),
let groups = String(data: groups, encoding: .utf8)
{
DispatchQueue.main.async { [events = self.events, groups] () in
events?(groups)
}
}
}
}
+45
View File
@@ -0,0 +1,45 @@
//
// AlertEventHandler.swift
// Runner
//
// Created by GFWFighter on 10/24/23.
//
import Foundation
import Combine
public class KRAlertsEventHandler: NSObject, FlutterPlugin, FlutterStreamHandler {
static let name = "\(Bundle.main.serviceIdentifier)/service.alerts"
private var channel: FlutterEventChannel?
private var cancellable: AnyCancellable?
public static func register(with registrar: FlutterPluginRegistrar) {
let instance = KRAlertsEventHandler()
instance.channel = FlutterEventChannel(name: Self.name, binaryMessenger: registrar.messenger(), codec: FlutterJSONMethodCodec())
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
}
}
+40
View File
@@ -0,0 +1,40 @@
//
// FileMethodHandler.swift
// Runner
//
// Created by GFWFighter on 10/23/23.
//
import Flutter
import Combine
import Libcore
public class KRFileMethodHandler: NSObject, FlutterPlugin {
public static let name = "\(Bundle.main.serviceIdentifier)/file"
public static func register(with registrar: FlutterPluginRegistrar) {
let channel = FlutterMethodChannel(name: Self.name, binaryMessenger: registrar.messenger())
let instance = KRFileMethodHandler()
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(kr_getPaths(args: call.arguments))
default:
result(FlutterMethodNotImplemented)
}
}
public func kr_getPaths(args: Any?) -> [String:String] {
return [
"base": FilePath.sharedDirectory.path,
"working": FilePath.workingDirectory.path,
"temp": FilePath.cacheDirectory.path
]
}
}
+50
View File
@@ -0,0 +1,50 @@
import Foundation
import Combine
import Libcore
public class KRGroupsEventHandler: NSObject, FlutterPlugin, FlutterStreamHandler{
static let name = "\(Bundle.main.serviceIdentifier)/groups"
private var commandClient: CommandClient?
private var channel: FlutterEventChannel?
private var events: FlutterEventSink?
private var cancellable: AnyCancellable?
public static func register(with registrar: FlutterPluginRegistrar) {
let instance = KRGroupsEventHandler()
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
commandClient = CommandClient(.groups)
commandClient?.connect()
cancellable = commandClient?.$groups.sink{ [self] groups in
self.kr_writeGroups(groups)
}
return nil
}
public func onCancel(withArguments arguments: Any?) -> FlutterError? {
commandClient?.disconnect()
cancellable?.cancel()
events = nil
return nil
}
func kr_writeGroups(_ sbGroups: [SBGroup]?) {
guard let sbGroups else {return}
if
let groups = try? JSONEncoder().encode(sbGroups),
let groups = String(data: groups, encoding: .utf8)
{
DispatchQueue.main.async { [events = self.events, groups] in
events?(groups)
}
}
}
}
+49
View File
@@ -0,0 +1,49 @@
import Foundation
import Combine
import Libcore
class KRLogsEventHandler: NSObject, FlutterPlugin, FlutterStreamHandler {
static let name = "\(Bundle.main.serviceIdentifier)/service.logs"
private var commandClient: CommandClient?
private var events: FlutterEventSink?
private var channel: FlutterEventChannel?
private var cancellable: AnyCancellable?
public static func register(with registrar: FlutterPluginRegistrar) {
let instance = KRLogsEventHandler()
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
commandClient = CommandClient(.log)
commandClient?.connect()
cancellable = commandClient?.$logList.sink{ [self] logs in
events(logs)
}
return nil
}
public func onCancel(withArguments arguments: Any?) -> FlutterError? {
commandClient?.disconnect()
cancellable?.cancel()
events = nil
return nil
}
}
/*
extension KRLogsEventHandler {
public func kr_clearLog() {}
public func kr_connected() {}
public func kr_disconnected(_ message: String?) {}
public func kr_initializeClashMode(_ modeList: LibboxStringIteratorProtocol?, currentMode: String?) {}
public func kr_updateClashMode(_ newMode: String?) {}
public func kr_writeGroups(_ message: LibboxOutboundGroupIteratorProtocol?) {}
public func kr_writeStatus(_ message: LibboxStatusMessage?) {}
}
*/
+232
View File
@@ -0,0 +1,232 @@
//
// MethodHandler.swift
// Runner
//
// Created by GFWFighter on 10/23/23.
//
import Flutter
import Combine
import Libcore
public class KRMethodHandler: NSObject, FlutterPlugin {
private var cancelBag: Set<AnyCancellable> = []
//
private var cancellables = Set<AnyCancellable>()
public static let name = "\(Bundle.main.serviceIdentifier)/method"
public static func register(with registrar: FlutterPluginRegistrar) {
let channel = FlutterMethodChannel(name: Self.name, binaryMessenger: registrar.messenger())
let instance = KRMethodHandler()
registrar.addMethodCallDelegate(instance, channel: channel)
instance.channel = channel
}
private var channel: FlutterMethodChannel?
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
@Sendable func kr_mainResult(_ res: Any?) async -> Void {
await MainActor.run {
result(res)
}
}
switch call.method {
case "parse_config":
guard
let args = call.arguments as? [String:Any?],
let path = args["path"] as? String,
let tempPath = args["tempPath"] as? String,
let debug = (args["debug"] as? NSNumber)?.boolValue
else {
result(FlutterError(code: "INVALID_ARGS", message: nil, details: nil))
return
}
var error: NSError?
MobileParse(path, tempPath, debug, &error)
if let error {
result(FlutterError(code: String(error.code), message: error.description, details: nil))
return
}
result("")
case "change_hiddify_options":
guard let options = call.arguments as? String else {
result(FlutterError(code: "INVALID_ARGS", message: nil, details: nil))
return
}
VPNConfig.shared.configOptions = options
result(true)
case "setup":
Task {
do {
try await VPNManager.shared.setup()
} catch {
await kr_mainResult(FlutterError(code: "SETUP", message: error.localizedDescription, details: nil))
return
}
await kr_mainResult(true)
}
case "start":
Task {
guard
let args = call.arguments as? [String:Any?],
let path = args["path"] as? String
else {
await kr_mainResult(FlutterError(code: "INVALID_ARGS", message: nil, details: nil))
return
}
VPNConfig.shared.activeConfigPath = path
var error: NSError?
let config = MobileBuildConfig(path, VPNConfig.shared.configOptions, &error)
print(config);
if let error {
await kr_mainResult(FlutterError(code: String(error.code), message: error.description, details: nil))
return
}
do {
try await VPNManager.shared.setup()
try await VPNManager.shared.connect(with: config, disableMemoryLimit: VPNConfig.shared.disableMemoryLimit)
} catch {
await kr_mainResult(FlutterError(code: "SETUP_CONNECTION", message: error.localizedDescription, details: nil))
return
}
await kr_mainResult(true)
}
case "restart":
Task { [unowned self] in
guard
let args = call.arguments as? [String:Any?],
let path = args["path"] as? String
else {
await kr_mainResult(FlutterError(code: "INVALID_ARGS", message: nil, details: nil))
return
}
VPNConfig.shared.activeConfigPath = path
VPNManager.shared.disconnect()
do {
try await kr_waitForStop().value
} catch {
await kr_mainResult(FlutterError(code: "SETUP_CONNECTION", message: error.localizedDescription, details: nil))
return
}
var error: NSError?
let config = MobileBuildConfig(path, VPNConfig.shared.configOptions, &error)
if let error {
await kr_mainResult(FlutterError(code: "BUILD_CONFIG", message: error.localizedDescription, details: nil))
return
}
do {
try await VPNManager.shared.setup()
try await VPNManager.shared.connect(with: config, disableMemoryLimit: VPNConfig.shared.disableMemoryLimit)
} catch {
await kr_mainResult(FlutterError(code: "SETUP_CONNECTION", message: error.localizedDescription, details: nil))
return
}
await kr_mainResult(true)
}
case "stop":
VPNManager.shared.disconnect()
result(true)
case "reset":
VPNManager.shared.reset()
result(true)
case "url_test":
guard
let args = call.arguments as? [String:Any?]
else {
result(FlutterError(code: "INVALID_ARGS", message: nil, details: nil))
return
}
let group = args["groupTag"] as? String
FileManager.default.changeCurrentDirectoryPath(FilePath.sharedDirectory.path)
do {
try LibboxNewStandaloneCommandClient()?.urlTest(group)
} catch {
result(FlutterError(code: "URL_TEST", message: error.localizedDescription, details: nil))
return
}
result(true)
case "select_outbound":
guard
let args = call.arguments as? [String:Any?],
let group = args["groupTag"] as? String,
let outbound = args["outboundTag"] as? String
else {
result(FlutterError(code: "INVALID_ARGS", message: nil, details: nil))
return
}
FileManager.default.changeCurrentDirectoryPath(FilePath.sharedDirectory.path)
do {
try LibboxNewStandaloneCommandClient()?.selectOutbound(group, outboundTag: outbound)
} catch {
result(FlutterError(code: "SELECT_OUTBOUND", message: error.localizedDescription, details: nil))
return
}
result(true)
case "generate_config":
guard
let args = call.arguments as? [String:Any?],
let path = args["path"] as? String
else {
result(FlutterError(code: "INVALID_ARGS", message: nil, details: nil))
return
}
var error: NSError?
let config = MobileBuildConfig(path, VPNConfig.shared.configOptions, &error)
if let error {
result(FlutterError(code: "BUILD_CONFIG", message: error.localizedDescription, details: nil))
return
}
result(config)
case "generate_warp_config":
guard let args = call.arguments as? [String: Any],
let licenseKey = args["license-key"] as? String,
let accountId = args["previous-account-id"] as? String,
let accessToken = args["previous-access-token"] as? String else {
result(FlutterError(code: "INVALID_ARGS", message: nil, details: nil))
return
}
let warpConfig = MobileGenerateWarpConfig(licenseKey, accountId, accessToken, nil)
result(warpConfig)
default:
result(FlutterMethodNotImplemented)
}
}
private func kr_waitForStop(timeout: TimeInterval = 1) -> Future<Void, Error> {
Future { promise in
print("开始等待 VPN 停止...")
let timeoutTimer = Timer.scheduledTimer(withTimeInterval: timeout, repeats: false) { _ in
promise(.failure(NSError(domain: "VPNError", code: -1, userInfo: [NSLocalizedDescriptionKey: "等待 VPN 停止超时"])))
}
VPNManager.shared.$state
.handleEvents(receiveSubscription: { _ in
print("订阅状态变化")
}, receiveOutput: { state in
print("当前 VPN 状态: \(state)")
})
.filter { $0 == .disconnected }
.first()
.delay(for: 0.5, scheduler: DispatchQueue.main)
.sink { _ in
timeoutTimer.invalidate()
print("VPN 已停止")
promise(.success(()))
}
.store(in: &self.cancellables)
}
}
}
+41
View File
@@ -0,0 +1,41 @@
//
// PlatformMethodHandler.swift
// Runner
//
// Created by Hiddify on 12/27/23.
//
import Flutter
import Combine
import Libcore
public class KRPlatformMethodHandler: NSObject, FlutterPlugin {
public static let name = "\(Bundle.main.serviceIdentifier)/platform"
public static func register(with registrar: FlutterPluginRegistrar) {
let channel = FlutterMethodChannel(name: Self.name, binaryMessenger: registrar.messenger())
let instance = KRPlatformMethodHandler()
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(kr_getPaths(args: call.arguments))
default:
result(FlutterMethodNotImplemented)
}
}
public func kr_getPaths(args: Any?) -> [String:String] {
return [
"base": FilePath.sharedDirectory.path,
"working": FilePath.workingDirectory.path,
"temp": FilePath.cacheDirectory.path
]
}
}
+54
View File
@@ -0,0 +1,54 @@
import Foundation
import Flutter
import Combine
import Libcore
public class KRStatsEventHandler: NSObject, FlutterPlugin, FlutterStreamHandler {
static let name = "\(Bundle.main.serviceIdentifier)/stats"
private var commandClient: CommandClient?
private var channel: FlutterEventChannel?
private var events: FlutterEventSink?
private var cancellable: AnyCancellable?
public static func register(with registrar: FlutterPluginRegistrar) {
let instance = KRStatsEventHandler()
instance.channel = FlutterEventChannel(name: Self.name,
binaryMessenger: registrar.messenger(),
codec: FlutterJSONMethodCodec())
instance.channel?.setStreamHandler(instance)
}
public func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
FileManager.default.changeCurrentDirectoryPath(FilePath.sharedDirectory.path)
self.events = events
commandClient = CommandClient(.status)
commandClient?.connect()
cancellable = commandClient?.$status.sink{ [self] status in
self.kr_writeStatus(status)
}
return nil
}
public func onCancel(withArguments arguments: Any?) -> FlutterError? {
commandClient?.disconnect()
cancellable?.cancel()
events = nil
return nil
}
func kr_writeStatus(_ message: LibboxStatusMessage?) {
guard let message else { return }
let data = [
"connections-in": message.connectionsIn,
"connections-out": message.connectionsOut,
"uplink": message.uplink,
"downlink": message.downlink,
"uplink-total": message.uplinkTotal,
"downlink-total": message.downlinkTotal
] as [String:Any]
events?(data)
}
}
+46
View File
@@ -0,0 +1,46 @@
//
// StatusEventHandler.swift
// Runner
//
// Created by GFWFighter on 10/24/23.
//
import Foundation
import Combine
public class KRStatusEventHandler: NSObject, FlutterPlugin, FlutterStreamHandler {
static let name = "\(Bundle.main.serviceIdentifier)/service.status"
private var channel: FlutterEventChannel?
private var cancellable: AnyCancellable?
public static func register(with registrar: FlutterPluginRegistrar) {
let instance = KRStatusEventHandler()
instance.channel = FlutterEventChannel(name: Self.name, binaryMessenger: registrar.messenger(), codec: FlutterJSONMethodCodec())
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
}
}
+85
View File
@@ -0,0 +1,85 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>BASE_BUNDLE_IDENTIFIER</key>
<string>$(BASE_BUNDLE_IDENTIFIER)</string>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>BearVPN</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>baer_with_panels</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>com.BearVPN.ios</string>
<key>CFBundleURLSchemes</key>
<array>
<string>hiddify</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>EXAppExtensionAttributes</key>
<dict>
<key>EXExtensionPointIdentifier</key>
<string>com.apple.appintents-extension</string>
</dict>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSCameraUsageDescription</key>
<string>需要相机权限以支持拍照功能</string>
<key>NSMicrophoneUsageDescription</key>
<string>需要麦克风权限以支持语音消息功能</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>需要相册权限以支持图片上传功能</string>
<key>SERVICE_IDENTIFIER</key>
<string>$(SERVICE_IDENTIFIER)</string>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UIRequiresFullScreen</key>
<true/>
<key>UIStatusBarHidden</key>
<false/>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
</dict>
</plist>
+41
View File
@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSPrivacyAccessedAPITypes</key>
<array>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>C617.1</string>
</array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryDiskSpace</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>E174.1</string>
</array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategorySystemBootTime</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>35F9.1</string>
</array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>1C8F.1</string>
</array>
</dict>
</array>
</dict>
</plist>
+1
View File
@@ -0,0 +1 @@
#import "GeneratedPluginRegistrant.h"
+22
View File
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>aps-environment</key>
<string>development</string>
<key>com.apple.developer.networking.networkextension</key>
<array>
<string>packet-tunnel-provider</string>
</array>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.application-groups</key>
<array>
<string>group.$(BASE_BUNDLE_IDENTIFIER)</string>
</array>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
</dict>
</plist>
+22
View File
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>aps-environment</key>
<string>development</string>
<key>com.apple.developer.networking.networkextension</key>
<array>
<string>packet-tunnel-provider</string>
</array>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.application-groups</key>
<array>
<string>group.app.baer.com</string>
</array>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
</dict>
</plist>
+86
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
}
}
}
}
+22
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
}
+214
View File
@@ -0,0 +1,214 @@
//
// VPNManager.swift
// Runner
//
// Created by GFWFighter on 7/25/1402 AP.
//
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> = []
private var observer: NSObjectProtocol?
private var manager = NEVPNManager.shared()
private var loaded: Bool = false
private var timer: Timer?
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
@Published private(set) var elapsedTime: TimeInterval = 0
private var _connectTime: Date?
private var connectTime: Date? {
set {
UserDefaults(suiteName: FilePath.groupName)?.set(newValue?.timeIntervalSince1970, forKey: "SingBoxConnectTime")
_connectTime = newValue
}
get {
if let _connectTime {
return _connectTime
}
guard let interval = UserDefaults(suiteName: FilePath.groupName)?.value(forKey: "SingBoxConnectTime") as? TimeInterval else {
return nil
}
return Date(timeIntervalSince1970: interval)
}
}
private var readingWS: Bool = false
@Published var isConnectedToAnyVPN: Bool = false
init() {
observer = NotificationCenter.default.addObserver(forName: .NEVPNStatusDidChange, object: nil, queue: nil) { [weak self] notification in
guard let connection = notification.object as? NEVPNConnection else { return }
self?.state = connection.status
}
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
guard let self else { return }
updateStats()
elapsedTime = -1 * (connectTime?.timeIntervalSinceNow ?? 0)
}
}
deinit {
if let observer {
NotificationCenter.default.removeObserver(observer)
}
timer?.invalidate()
}
func setup() async throws {
// guard !loaded else { return }
loaded = true
do {
try await loadVPNPreference()
} catch {
print(error.localizedDescription)
}
}
private func loadVPNPreference() async throws {
do {
let managers = try await NETunnelProviderManager.loadAllFromPreferences()
if let manager = managers.first {
self.manager = manager
return
}
let newManager = NETunnelProviderManager()
let `protocol` = NETunnelProviderProtocol()
`protocol`.providerBundleIdentifier = Bundle.main.baseBundleIdentifier + ".PacketTunnel"
`protocol`.serverAddress = "localhost"
newManager.protocolConfiguration = `protocol`
newManager.localizedDescription = "BearVPN"
try await newManager.saveToPreferences()
try await newManager.loadFromPreferences()
self.manager = newManager
} catch {
print(error.localizedDescription)
}
}
private func enableVPNManager() async throws {
manager.isEnabled = true
do {
try await manager.saveToPreferences()
try await manager.loadFromPreferences()
} catch {
print(error.localizedDescription)
}
}
@MainActor private func set(upload: Int64, download: Int64) {
self.upload = upload
self.download = download
}
var isAnyVPNConnected: Bool {
guard let cfDict = CFNetworkCopySystemProxySettings() else { return false }
let nsDict = cfDict.takeRetainedValue() as NSDictionary
guard let keys = nsDict["__SCOPED__"] as? NSDictionary else {
return false
}
for key: String in keys.allKeys as! [String] {
if key == "tap" || key == "tun" || key == "ppp" || key == "ipsec" || key == "ipsec0" {
return true
} else if key.starts(with: "utun") {
return true
}
}
return false
}
func reset() {
loaded = false
if state != .disconnected && state != .invalid {
disconnect()
}
$state.filter { $0 == .disconnected || $0 == .invalid }.first().sink { [weak self] _ in
Task { [weak self] () in
self?.manager = .shared()
do {
let managers = try await NETunnelProviderManager.loadAllFromPreferences()
for manager in managers ?? [] {
try await manager.removeFromPreferences()
}
try await self?.loadVPNPreference()
} catch {
print(error.localizedDescription)
}
}
}.store(in: &cancelBag)
}
private func updateStats() {
let isAnyVPNConnected = self.isAnyVPNConnected
if isConnectedToAnyVPN != isAnyVPNConnected {
isConnectedToAnyVPN = isAnyVPNConnected
}
guard state == .connected else { return }
guard let connection = manager.connection as? NETunnelProviderSession else { return }
do {
try connection.sendProviderMessage("stats".data(using: .utf8)!) { [weak self] response in
guard
let response,
let response = String(data: response, encoding: .utf8)
else { return }
let responseComponents = response.components(separatedBy: ",")
guard
responseComponents.count == 2,
let upload = Int64(responseComponents[0]),
let download = Int64(responseComponents[1])
else { return }
Task { [upload, download, weak self] () in
await self?.set(upload: upload, download: download)
}
}
} catch {
print(error.localizedDescription)
}
}
func connect(with config: String, disableMemoryLimit: Bool = false) async throws {
await set(upload: 0, download: 0)
guard state == .disconnected else { return }
do {
try await enableVPNManager()
try manager.connection.startVPNTunnel(options: [
"Config": config as NSString,
"DisableMemoryLimit": (disableMemoryLimit ? "YES" : "NO") as NSString,
])
} catch {
print(error.localizedDescription)
}
connectTime = .now
}
func disconnect() {
guard state == .connected else { return }
manager.connection.stopVPNTunnel()
}
}