punchnet-macos/punchnet/Views/AppContext.swift

216 lines
6.1 KiB
Swift

//
// LoginState.swift
// punchnet
//
// Created by on 2026/1/16.
//
import Foundation
import Observation
struct AppContextError: Error {
let message: String
}
@Observable
class AppContext {
private var vpnManager = VPNManager.shared
// "/connect"
var networkContext: SDLAPIClient.NetworkContext? = nil
// menu使
var vpnOptions: [String: NSObject]? = nil
// app
var appScene: AppScene = .login(username: nil)
//
enum AppScene: Equatable {
case login(username: String?)
case logined
case register
case resetPassword
}
//
var loginCredit: Credit?
//
var isLogined: Bool {
return loginCredit != nil
}
enum Credit {
case token(token: String, session: SDLAPIClient.NetworkSession)
case accountAndPasword(account: String, password: String, session: SDLAPIClient.NetworkSession)
}
@ObservationIgnored
var networkSession: SDLAPIClient.NetworkSession? {
guard let loginCredit = self.loginCredit else {
return nil
}
switch loginCredit {
case .token(_, let session):
return session
case .accountAndPasword(_, _, let session):
return session
}
}
var tunnelEvent: SDLTunnelAppEventStore.Event? = nil
@ObservationIgnored
private var lastTunnelEventID: String?
init() {
self.observeTunnelEvent()
self.consumeLatestTunnelEvent()
}
func loginWith(token: String) async throws {
let networkSession = try await SDLAPIClient.loginWithToken(token: token)
self.loginCredit = .token(token: token, session: networkSession)
// keychain
if let data = token.data(using: .utf8) {
try KeychainStore.shared.save(data, account: "token")
}
}
func loginWith(username: String, password: String) async throws {
let networkSession = try await SDLAPIClient.loginWithAccountAndPassword(username: username, password: password)
self.loginCredit = .accountAndPasword(account: username, password: password, session: networkSession)
// keychain
if let data = "\(username):\(password)".data(using: .utf8) {
try KeychainStore.shared.save(data, account: "accountAndPasword")
}
}
//
func connectNetwork() async throws {
guard let session = self.networkSession else {
throw AppContextError(message: "未登陆")
}
//
guard !vpnManager.isConnected else {
throw AppContextError(message: "网络已经连接")
}
self.networkContext = try await SDLAPIClient.connectNetwork(accesToken: session.accessToken)
}
func changeExitNodeIp(exitNodeIp: String) async throws -> Data {
//
guard vpnManager.isConnected else {
throw AppContextError(message: "网络未连接")
}
var exitNodeIpChanged = NEMessage.ExitNodeIpChanged()
exitNodeIpChanged.ip = exitNodeIp
var neMessage = NEMessage()
neMessage.message = .exitNodeIpChanged(exitNodeIpChanged)
let message = try neMessage.serializedData()
return try await self.vpnManager.sendMessage(message)
}
// tun
func startTun() async throws {
guard let session = self.networkSession, let context = self.networkContext else {
return
}
let options = SystemConfig.getOptions(
networkId: UInt32(session.networkId),
networkDomain: session.networkDomain,
ip: context.ip,
maskLen: context.maskLen,
accessToken: session.accessToken,
identityId: context.identityId,
hostname: context.hostname,
exitNodeIp: self.loadExitNodeIp()
)
try await self.vpnManager.enableVpn(options: options)
}
//
func stopTun() async throws {
try await self.vpnManager.disableVpn()
}
// 退
func logout() async throws {
try await self.vpnManager.disableVpn()
self.networkContext = nil
self.loginCredit = nil
}
func loadCacheToken() -> String? {
if let data = try? KeychainStore.shared.load(account: "token") {
return String(data: data, encoding: .utf8)
}
return nil
}
func loadCacheUsernameAndPassword() -> (String, String)? {
if let data = try? KeychainStore.shared.load(account: "accountAndPasword"),
let str = String(data: data, encoding: .utf8) {
let parts = str.split(separator: ":")
if parts.count == 2 {
return (String(parts[0]), String(parts[1]))
}
}
return nil
}
}
//
extension AppContext {
func loadExitNodeIp() -> String? {
if let data = try? KeychainStore.shared.load(account: "exitNodeIp") {
return String(data: data, encoding: .utf8)
}
return nil
}
func saveExitNodeIp(exitNodeIp: String) async throws {
// keychain
if let data = exitNodeIp.data(using: .utf8) {
try KeychainStore.shared.save(data, account: "exitNodeIp")
}
}
func dismissTunnelEvent() {
self.tunnelEvent = nil
}
private func observeTunnelEvent() {
SDLNotificationCenter.shared.addObserver(for: .tunnelEventChanged) { [weak self] _ in
self?.consumeLatestTunnelEvent()
}
}
private func consumeLatestTunnelEvent() {
guard let event = SDLTunnelAppEventStore.loadLatestEvent(),
self.lastTunnelEventID != event.id else {
return
}
self.lastTunnelEventID = event.id
self.tunnelEvent = event
SDLTunnelAppEventStore.clearLatestEvent()
}
deinit {
SDLNotificationCenter.shared.removeObserver(for: .tunnelEventChanged)
}
}