Files
ios-client/NetBird/Source/App/ViewModels/MainViewModel.swift
T
2024-04-30 11:45:27 +02:00

301 lines
11 KiB
Swift

//
// MainViewModel.swift
// NetBirdiOS
//
// Created by Pascal Fischer on 01.08.23.
//
import UIKit
import NetworkExtension
import os
import Combine
@MainActor
class ViewModel: ObservableObject {
@Published var networkExtensionAdapter = NetworkExtensionAdapter()
@Published var showSetupKeyPopup = false
@Published var showChangeServerAlert = false
@Published var showInvalidServerAlert = false
@Published var showInvalidSetupKeyHint = false
@Published var showInvalidSetupKeyAlert = false
@Published var showLogLevelChangedAlert = false
@Published var showInvalidPresharedKeyAlert = false
@Published var showServerChangedInfo = false
@Published var showPreSharedKeyChangedInfo = false
@Published var showCopiedAlert = false
@Published var showCopiedInfoAlert = false
@Published var showAuthenticationRequired = false
@Published var presentSideDrawer = false
@Published var extensionState : NEVPNStatus = .disconnected
@Published var navigateToServerView = false
@Published var rosenpassEnabled = false
@Published var rosenpassPermissive = false
@Published var managementURL = ""
@Published var presharedKey = ""
@Published var server: String = ""
@Published var setupKey: String = ""
@Published var presharedKeySecure = true
@Published var statusDetails = StatusDetails(ip: "", fqdn: "", managementStatus: .disconnected, peerInfo: [])
@Published var statusDetailsValid = false
@Published var extensionStateText = "Disconnected"
@Published var connectPressed = false
@Published var disconnectPressed = false
@Published var traceLogsEnabled: Bool {
didSet {
self.showLogLevelChangedAlert = true
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
self.showLogLevelChangedAlert = false
}
let logLevel = traceLogsEnabled ? "TRACE" : "INFO"
UserDefaults.standard.set(logLevel, forKey: "logLevel")
UserDefaults.standard.synchronize()
}
}
var preferences = Preferences.newPreferences()
var buttonLock = false
let defaults = UserDefaults.standard
@Published var fqdn = UserDefaults.standard.string(forKey: "fqdn") ?? ""
@Published var ip = UserDefaults.standard.string(forKey: "ip") ?? ""
private var cancellables = Set<AnyCancellable>()
init() {
let logLevel = UserDefaults.standard.string(forKey: "logLevel") ?? "INFO"
self.traceLogsEnabled = logLevel == "TRACE"
self.rosenpassEnabled = self.getRosenpassEnabled()
self.rosenpassPermissive = self.getRosenpassPermissive()
$server
.removeDuplicates()
.debounce(for: .seconds(0.5), scheduler: RunLoop.main)
.map { server in
!self.isValidURL(server)
}
.assign(to: &$showInvalidServerAlert)
$setupKey
.removeDuplicates()
.debounce(for: .seconds(0.5), scheduler: RunLoop.main)
.map { setupKey in
!self.isValidSetupKey(setupKey)
}
.assign(to: &$showInvalidSetupKeyHint)
}
func connect() {
self.connectPressed = true
print("Connected pressed set to true")
DispatchQueue.main.async {
print("starting extension")
self.buttonLock = true
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
self.buttonLock = false
}
Task {
await self.networkExtensionAdapter.start()
print("Connected pressed set to false")
}
}
}
func close() -> Void {
self.disconnectPressed = true
DispatchQueue.main.async {
print("Stopping extension")
self.buttonLock = true
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
self.buttonLock = false
}
self.networkExtensionAdapter.stop()
}
}
func startPollingDetails() {
networkExtensionAdapter.startTimer { details in
let sortedPeerInfo = details.peerInfo.sorted(by: { a, b in
a.ip < b.ip
})
self.checkExtensionState()
if self.extensionState == .disconnected && self.extensionStateText == "Connected" {
self.showAuthenticationRequired = true
self.extensionStateText = "Disconnected"
}
let newStatusDetails = StatusDetails(ip: details.ip, fqdn: details.fqdn, managementStatus: details.managementStatus, peerInfo: sortedPeerInfo)
if newStatusDetails.ip != self.statusDetails.ip || newStatusDetails.fqdn != self.statusDetails.fqdn || newStatusDetails.managementStatus != self.statusDetails.managementStatus || !newStatusDetails.peerInfo.elementsEqual(self.statusDetails.peerInfo, by: { a, b in
a.ip == b.ip && a.connStatus == b.connStatus
}) {
if !newStatusDetails.fqdn.isEmpty && newStatusDetails.fqdn != self.statusDetails.fqdn {
self.defaults.set(newStatusDetails.fqdn, forKey: "fqdn")
self.fqdn = details.fqdn
}
if !newStatusDetails.ip.isEmpty && newStatusDetails.ip != self.statusDetails.ip {
self.defaults.set(newStatusDetails.ip, forKey: "ip")
self.ip = details.ip
}
print("Status: \(newStatusDetails.managementStatus) - Extension: \(self.extensionState) - LoginRequired: \(self.networkExtensionAdapter.isLoginRequired())")
if newStatusDetails.managementStatus == .disconnected && self.extensionState == .connected && self.networkExtensionAdapter.isLoginRequired() {
self.networkExtensionAdapter.stop()
self.showAuthenticationRequired = true
}
self.statusDetails = newStatusDetails
}
self.statusDetailsValid = true
}
}
func stopPollingDetails() {
networkExtensionAdapter.stopTimer()
}
func checkExtensionState() {
networkExtensionAdapter.getExtensionStatus { status in
let statuses : [NEVPNStatus] = [.connected, .disconnected, .connecting, .disconnecting]
DispatchQueue.main.async {
if statuses.contains(status) && self.extensionState != status {
print("Changing extension status")
self.extensionState = status
}
}
}
}
func updateManagementURL(url: String) -> Bool? {
let trimmedURL = url.trimmingCharacters(in: .whitespacesAndNewlines)
let newAuth = NetBirdSDKNewAuth(Preferences.configFile(), trimmedURL, nil)
self.managementURL = trimmedURL
var ssoSupported: ObjCBool = false
do {
try newAuth?.saveConfigIfSSOSupported(&ssoSupported)
if ssoSupported.boolValue {
print("SSO is supported")
return true
} else {
print("SSO is not supported. Fallback to setup key")
return false
}
} catch {
print("Failed to check SSO support")
}
return nil
}
func clearDetails() {
self.ip = ""
self.fqdn = ""
defaults.removeObject(forKey: "ip")
defaults.removeObject(forKey: "fqdn")
}
func setSetupKey(key: String) throws {
let newAuth = NetBirdSDKNewAuth(Preferences.configFile(), self.managementURL, nil)
try newAuth?.login(withSetupKeyAndSaveConfig: key, deviceName: Device.getName())
self.managementURL = ""
}
func updatePreSharedKey() {
preferences.setPreSharedKey(presharedKey)
do {
try preferences.commit()
self.close()
self.presharedKeySecure = true
self.presentSideDrawer = false
self.showPreSharedKeyChangedInfo = true
} catch {
print("Failed to update preshared key")
}
}
func removePreSharedKey() {
presharedKey = ""
preferences.setPreSharedKey(presharedKey)
do {
try preferences.commit()
self.close()
self.presharedKeySecure = false
} catch {
print("Failed to remove preshared key")
}
}
func loadPreSharedKey() {
self.presharedKey = preferences.getPreSharedKey(nil)
self.presharedKeySecure = self.presharedKey != ""
}
func setRosenpassEnabled(enabled: Bool) {
preferences.setRosenpassEnabled(enabled)
do {
try preferences.commit()
} catch {
print("Failed to update rosenpass settings")
}
}
func getRosenpassEnabled() -> Bool {
var result = ObjCBool(false)
do {
try preferences.getRosenpassEnabled(&result)
} catch {
print("Failed to read rosenpass settings")
}
return result.boolValue
}
func getRosenpassPermissive() -> Bool {
var result = ObjCBool(false)
do {
try preferences.getRosenpassPermissive(&result)
} catch {
print("Failed to read rosenpass permissive settings")
}
return result.boolValue
}
func setRosenpassPermissive(permissive: Bool) {
preferences.setRosenpassPermissive(permissive)
do {
try preferences.commit()
} catch {
print("Failed to update rosenpass permissive settings")
}
}
func getDefaultStatus() -> StatusDetails {
return StatusDetails(ip: "", fqdn: "", managementStatus: .disconnected, peerInfo: [])
}
func isValidURL(_ string: String) -> Bool {
let trimmedString = string.trimmingCharacters(in: .whitespacesAndNewlines)
if trimmedString.isEmpty { return true }
let pattern = "^(?i)https?://(([a-zA-Z\\d]([a-zA-Z\\d-]{0,61}[a-zA-Z\\d])?\\.)*[a-zA-Z]{2,})(?::\\d{1,5})?(?:/|$)"
let isMatch = trimmedString.range(of: pattern, options: .regularExpression, range: nil, locale: nil) != nil
return isMatch
}
func isValidSetupKey(_ string: String) -> Bool {
if string.isEmpty { return true }
let pattern = "^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$"
let isMatch = string.range(of: pattern, options: .regularExpression, range: nil, locale: nil) != nil
return isMatch
}
}
func printLogContents(from logURL: URL) {
do {
let logContents = try String(contentsOf: logURL, encoding: .utf8)
print(logContents)
} catch {
print("Failed to read the log file: \(error.localizedDescription)")
}
}