punchnet-macos/punchnet/App/AppContext.swift

276 lines
8.3 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// 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
// IP nil
var selectedExitNodeIp: String? = 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)
self.selectedExitNodeIp = self.loadExitNodeIp(networkId: networkSession.networkId)
// 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)
self.selectedExitNodeIp = self.loadExitNodeIp(networkId: networkSession.networkId)
// 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 changeExitNode = AppRequest.ChangeExitNodeRequest()
changeExitNode.ip = exitNodeIp
var appRequest = AppRequest()
appRequest.command = .changeExitNode(changeExitNode)
let message = try appRequest.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.selectedExitNodeIp
)
try await self.vpnManager.enableVpn(options: options)
}
//
func stopTun() async throws {
try await self.vpnManager.disableVpn()
self.networkContext = nil
}
// 退
func logout() async throws {
try await self.vpnManager.disableVpn()
self.networkContext = nil
self.selectedExitNodeIp = 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
}
// MARK: TunEvent
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)
}
}
//
extension AppContext {
private func exitNodeStorageAccount(networkId: Int?) -> String {
if let networkId {
return "exitNodeIp_\(networkId)"
}
return "exitNodeIp"
}
func loadExitNodeIp(networkId: Int? = nil) -> String? {
let account = self.exitNodeStorageAccount(networkId: networkId)
if let data = try? KeychainStore.shared.load(account: account) {
return String(data: data, encoding: .utf8)
}
if networkId != nil,
let data = try? KeychainStore.shared.load(account: self.exitNodeStorageAccount(networkId: nil)) {
return String(data: data, encoding: .utf8)
}
return nil
}
func saveExitNodeIp(exitNodeIp: String, networkId: Int? = nil) async throws {
let account = self.exitNodeStorageAccount(networkId: networkId)
// keychain
if let data = exitNodeIp.data(using: .utf8) {
try KeychainStore.shared.save(data, account: account)
}
}
func clearExitNodeIp(networkId: Int? = nil) throws {
try KeychainStore.shared.delete(account: self.exitNodeStorageAccount(networkId: networkId))
}
func updateExitNodeIp(exitNodeIp: String?) async throws {
let normalizedExitNodeIp = exitNodeIp?.trimmingCharacters(in: .whitespacesAndNewlines)
let finalExitNodeIp = normalizedExitNodeIp?.isEmpty == true ? nil : normalizedExitNodeIp
let networkId = self.networkSession?.networkId
if vpnManager.isConnected {
let result = try await self.changeExitNodeIp(exitNodeIp: finalExitNodeIp ?? "0.0.0.0")
let reply = try TunnelResponse(serializedBytes: result)
guard reply.code == 0 else {
throw AppContextError(message: reply.message)
}
}
if let finalExitNodeIp {
try await self.saveExitNodeIp(exitNodeIp: finalExitNodeIp, networkId: networkId)
} else {
try self.clearExitNodeIp(networkId: networkId)
}
if networkId != nil {
try? self.clearExitNodeIp(networkId: nil)
}
self.selectedExitNodeIp = finalExitNodeIp
}
}