382 lines
13 KiB
Swift
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)
|
|
}
|
|
}
|
|
|
|
}
|