276 lines
8.2 KiB
Swift
276 lines
8.2 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: 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: NetworkSession)
|
||
case accountAndPasword(account: String, password: String, session: NetworkSession)
|
||
}
|
||
|
||
@ObservationIgnored
|
||
var networkSession: 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
|
||
}
|
||
|
||
|
||
|
||
}
|