Files

382 lines
13 KiB
Swift

//
// VPNAssistant.swift
// PrivadoVPN
//
// Created by Juraldinio on 3/24/21.
// Copyright © 2021 Privado LLC. All rights reserved.
//
import Foundation
final class VPNAssistant {
typealias ConnectionEmitterTuple = (VPNAssistant, VPNConnectionState)
private var current: Server?
private var requiredServer: Server?
private var alreadyReinstalled = false
private(set) var isReconnection = false
private(set) var connectionState = VPNConnectionState.initialize
private let session: Session?
private let keychain: KeychainSwift
private var changingService: Future<Void>?
var hasRequiredServer: Bool { self.requiredServer.isExist }
let connectionStateEmitter = Emitter<ConnectionEmitterTuple>()
// swiftlint:disable function_body_length
init(session: Session?, keychain: KeychainSwift) {
self.session = session
self.keychain = keychain
//
UserSettings.shared.vpnTypeEmitter.addReaction { [weak self] _ in
guard let self = self else { return false }
guard let server = self.current else { return true }
self.current = nil
self.selected(server: server)
return true
}
//
session?.vpnConnectionStateEmitter.addReaction { [weak self] state, _ -> ShouldContinueReceiveNotifications in
guard let self = self else { return false }
let currentState: VPNConnectionState
if case let .reconnect(state) = self.connectionState {
currentState = state
} else {
currentState = self.connectionState
}
guard currentState != state else { return true }
switch state {
case .connecting,
.disconnecting:
self.alreadyReinstalled = false
case .disconnected:
if let server = self.requiredServer {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.42) {
self.requiredServer = nil
self.connect(using: server)
}
}
let isContains = [VPNConnectionState.connecting, VPNConnectionState.connected,
VPNConnectionState.disconnecting(true), VPNConnectionState.disconnecting(false)].contains(currentState)
if isContains {
CometLogger.shared.event(name: PrivadoConstants.Event.Statistic.VPN.connect,
attributes: [
PrivadoConstants.Event.Attributes.isOn: false
],
secured: nil)
}
case .connected:
self.isReconnection = false
CometLogger.shared.event(name: PrivadoConstants.Event.Statistic.VPN.connect,
attributes: [
PrivadoConstants.Event.Attributes.isOn: true
],
secured: nil)
case let .error(error):
self.handleVPN(error: error)
default:
break
}
self.connectionState = self.isReconnection ? .reconnect(state) : state
self.connectionStateEmitter.invoke((self, self.connectionState))
return true
}
//
PrivadoHiveClient.shared().stateEmitter.addReaction { [weak self] state -> ShouldContinueReceiveNotifications in
guard let self = self else { return false }
if state == .connected
, self.isReconnection
, let server = self.requiredServer {
self.connect(using: server)
}
return true
}
}
// swiftlint:enable function_body_length
// MARK: - Internal
func isDifferent(server: Server) -> Bool {
return self.current != server
}
func selected(server: Server) {
guard self.current != server,
let service = VPNAssistant.switchVPNService(using: UserSettings.shared.vpnType ?? .ikev2) else {
return
}
let currentService = self.session?.currentVPN
self.changingService = service
.install()
.onSuccess { [weak self] in
self?.changingService = nil
self?.current = server
self?.session?.switchVPNService(service)
UserDefaults.standard.set(true, forKey: service.id)
}
.onFailure { [weak self] _ in
self?.changingService = nil
if let self = self,
let service = currentService,
service.isEquals(service: self.session?.currentVPN) {
UserSettings.shared.vpnType = service.type
UserDefaults.standard.set(false, forKey: service.id)
}
}
}
func connect() {
let future = self.changingService ?? Future(value: ())
future.onSuccess { [weak self] in
if UserSettings.shared.vpnType != .automatic {
self?.performConnection()
} else {
self?.manageAutomaticConnection()
}
}
}
func disconnect() {
guard let service = self.session?.currentVPN else { return }
service.disconnect()
}
func reconnect(to server: Server) {
self.requiredServer = server
self.isReconnection = true
self.disconnect()
}
// MARK: - Private
private func manageAutomaticConnection() {
let priorityService = ProtocolsPriorityService()
if let proto = priorityService.nextProtocol() {
self.switchAutomaticService(proto: proto)
self.performConnection()
}
session?.vpnConnectionStateEmitter.addReaction { [weak self] state, _ -> ShouldContinueReceiveNotifications in
guard let self = self else { return false }
switch state {
case .connected:
return false
case .error:
if let proto = priorityService.nextProtocol() {
self.switchAutomaticService(proto: proto)
self.performConnection()
}
default:
break
}
if !priorityService.hasNextProtocol() {
return false
}
return true
}
}
private func switchAutomaticService(proto: ConnectionPriorityType) {
if let currentVPN = session?.currentVPN
, currentVPN.state == .connected {
return
}
var service: VPNService?
let currentType: VPNType = {
switch proto {
case ConnectionPriorityType.ikev2:
return VPNType.ikev2
case ConnectionPriorityType.ovpn(_,_):
return VPNType.openVPN
default:
return VPNType.ikev2
}
}()
if currentType == .openVPN {
if let settings = proto.ovpnSettings() {
UserSettings.shared.proto = settings.proto.rawValue
UserSettings.shared.port = settings.port.rawValue
}
}
switch currentType {
case .ikev2:
service = IKEv2Service(with: CometLogger.shared)
case .openVPN:
service = OpenVPNService(with: CometLogger.shared)
case .wireGuard:
if #available(macOS 10.15, *) {
service = WireGuardService(with: CometLogger.shared)
} else {
service = nil
}
default:
break
}
if let service = service {
self.session?.switchVPNService(service)
}
}
private func performConnection() {
guard let server = self.current
, let service = self.session?.currentVPN else {
return
}
let username: String
let pwd: String
if let user = self.keychain.get(PrivadoConstants.Keychain.username)
, let password = self.keychain.get(PrivadoConstants.Keychain.passwd) {
username = user
pwd = password
CometLogger.shared.event(name: PrivadoConstants.Event.source, attributes: ["Credentials": "Keychain"], secured: nil)
} else if let user = self.session?.currentRecord?.username
, let password = self.session?.currentRecord?.password {
username = user
pwd = password
CometLogger.shared.event(name: PrivadoConstants.Event.source, attributes: ["Credentials": "Session"], secured: nil)
} else {
CometLogger.shared.event(name: PrivadoConstants.Event.source, attributes: ["Credentials": "Failed"], secured: nil)
openApplicationRoute(.login(type: .clear))
return
}
let config: VPNConfiguration
switch service.type {
case .ikev2:
config = .ikev2(spot: VPNConfiguration.Spot(username: username,
password: pwd,
city: server.city,
server: server.name,
serverIp: server.ip),
shared: "",
remoteIdentifier: "vpn.privado.io")
#if SIMULATOR
#else
case .openVPN:
if let proto = server.protocols
.filter({ $0.name.lowercased() == service.type.description.lowercased() })
.filter({ $0.socketType.lowercased() == UserSettings.shared.proto.lowercased() })
.filter({ $0.port == UserSettings.shared.port }).first {
config = .ovpn(spot: VPNConfiguration.Spot(username: username, password: pwd, city: server.city, server: server.name, serverIp: server.ip),
port: UInt16(proto.port),
socketType: proto.socketType)
} else {
config = .ovpn(spot: VPNConfiguration.Spot(username: username, password: pwd, city: server.city, server: server.name, serverIp: server.ip),
port: UInt16(1194),
socketType: "UDP")
}
#endif
case .wireGuard:
config = .wireguard(spot: VPNConfiguration.Spot(username: username,
password: pwd,
city: server.city,
server: server.name,
serverIp: server.ip))
default:
config = .ikev2(spot: VPNConfiguration.Spot(username: username, password: pwd, city: server.city, server: server.name, serverIp: server.ip),
shared: "",
remoteIdentifier: "vpn.privado.io")
}
CometLogger.shared.vpnEvent(type: PrivadoConstants.Event.config, userInfo: nil, secured: String(describing: config))
service.connect(using: config)
}
/*func reinstall() {
self.requiredServer = self.current
self.isReconnection = true
PrivadoHiveClient.shared().forceUpdate()
openApplicationRoute(.hive(type: .install))
}*/
private func connect(using server: Server) {
DispatchQueue.main.asyncAfter(deadline: .now() + 5) { [weak self] in
self?.selected(server: server)
self?.connect()
}
}
private func handleVPN(error: VPNError) {
/*switch error {
case .requireGrantsUpdate,
.communication:
self.allowClick = true
if !self.alreadyReinstalled {
self.alreadyReinstalled.toggle()
self.output?.vpnReinstall()
}
default:
self.allowClick = true
}
if let view = self.viewInput {
self.update(view: view, using: self.connectionState, allowClick: self.allowClick)
}*/
}
// MARK: - Static
private static func switchVPNService(using type: VPNType) -> VPNService? {
switch type {
case .ikev2: return IKEv2Service(with: CometLogger.shared)
case .openVPN: return OpenVPNService(with: CometLogger.shared)
case .wireGuard:
if #available(macOS 10.15, *) {
return WireGuardService(with: CometLogger.shared)
} else {
return nil
}
case .automatic: return IKEv2Service(with: CometLogger.shared)
}
}
}