初始化提交
This commit is contained in:
Executable
+86
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+22
@@ -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
|
||||
}
|
||||
Executable
+214
@@ -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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user