初始化提交
@@ -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)!)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 85 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 7.7 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 8.5 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 5.9 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 69 B |
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
After Width: | Height: | Size: 6.8 KiB |
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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)!
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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?) {}
|
||||
}
|
||||
*/
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -0,0 +1 @@
|
||||
#import "GeneratedPluginRegistrant.h"
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||