diff --git a/SolardVPNCommunityCoreiOS/SOLARAPI/Providers/NodesProvider/NodesAPITarget.swift b/SolardVPNCommunityCoreiOS/SOLARAPI/Providers/NodesProvider/NodesAPITarget.swift index 372ba60..111bcac 100644 --- a/SolardVPNCommunityCoreiOS/SOLARAPI/Providers/NodesProvider/NodesAPITarget.swift +++ b/SolardVPNCommunityCoreiOS/SOLARAPI/Providers/NodesProvider/NodesAPITarget.swift @@ -31,7 +31,7 @@ extension NodesAPITarget: APITarget { case .getNodes: return "dvpn/getNodes" case .postNodesByAddress: - return "dvpn/postNodesByAddress" + return "dvpn/getNodesByAddress" case .getCountries: return "dvpn/getCountries" } diff --git a/SolardVPNCommunityCoreiOS/SOLARAPI/Providers/SessionProvider/Models/StartSessionRequest.swift b/SolardVPNCommunityCoreiOS/SOLARAPI/Providers/SessionProvider/Models/StartSessionRequest.swift new file mode 100644 index 0000000..a9f0b61 --- /dev/null +++ b/SolardVPNCommunityCoreiOS/SOLARAPI/Providers/SessionProvider/Models/StartSessionRequest.swift @@ -0,0 +1,18 @@ +// +// StartSessionRequest.swift +// SOLARAPI +// +// Created by Lika Vorobeva on 26.09.2022. +// + +import Foundation + +public struct StartSessionRequest: Codable { + public let key: String + public let signature: String + + public init(key: String, signature: String) { + self.key = key + self.signature = signature + } +} diff --git a/SolardVPNCommunityCoreiOS/SOLARAPI/Providers/SessionProvider/Models/StartSessionResponse.swift b/SolardVPNCommunityCoreiOS/SOLARAPI/Providers/SessionProvider/Models/StartSessionResponse.swift new file mode 100644 index 0000000..dddbc33 --- /dev/null +++ b/SolardVPNCommunityCoreiOS/SOLARAPI/Providers/SessionProvider/Models/StartSessionResponse.swift @@ -0,0 +1,13 @@ +// +// StartSessionResponse.swift +// SOLARAPI +// +// Created by Lika Vorobeva on 26.09.2022. +// + +import Foundation + +public struct StartSessionResponse: Codable { + public let success: Bool + public let result: String? +} diff --git a/SolardVPNCommunityCoreiOS/SOLARAPI/Providers/SessionProvider/NodeSessionAPITarget.swift b/SolardVPNCommunityCoreiOS/SOLARAPI/Providers/SessionProvider/NodeSessionAPITarget.swift new file mode 100644 index 0000000..7766c81 --- /dev/null +++ b/SolardVPNCommunityCoreiOS/SOLARAPI/Providers/SessionProvider/NodeSessionAPITarget.swift @@ -0,0 +1,36 @@ +// +// NodeSessionAPITarget.swift +// SOLARAPI +// +// Created by Lika Vorobeva on 26.09.2022. +// + +import Foundation +import Alamofire + +enum NodeSessionAPITarget { + case createClient(address: String, id: String, request: StartSessionRequest) +} + +extension NodeSessionAPITarget: APITarget { + var method: HTTPMethod { + switch self { + case .createClient: + return .post + } + } + + var path: String { + switch self { + case let .createClient(address, id, _): + return "accounts/\(address)/sessions/\(id)" + } + } + + var parameters: Parameters { + switch self { + case let .createClient(_, _, data): + return .requestJSONEncodable(data) + } + } +} diff --git a/SolardVPNCommunityCoreiOS/SOLARAPI/Providers/SessionProvider/NodeSessionProvider.swift b/SolardVPNCommunityCoreiOS/SOLARAPI/Providers/SessionProvider/NodeSessionProvider.swift new file mode 100644 index 0000000..9bdfeb7 --- /dev/null +++ b/SolardVPNCommunityCoreiOS/SOLARAPI/Providers/SessionProvider/NodeSessionProvider.swift @@ -0,0 +1,34 @@ +// +// NodeSessionProvider.swift +// SOLARAPI +// +// Created by Lika Vorobeva on 26.09.2022. +// + +import Foundation +import Alamofire + +public final class NodeSessionProvider: SOLARAPIProvider { + public init() {} +} + +// MARK: - StealthProviderType implementation + +extension NodeSessionProvider: NodeSessionProviderType { + public func createClient( + remoteURL: URL, + address: String, + id: String, + request: StartSessionRequest, + completion: @escaping (Result) -> Void + ) { + let apiRequest = APIRequest( + baseURL: remoteURL, + target: NodeSessionAPITarget.createClient(address: address, id: id, request: request) + ) + + AF.request(apiRequest) + .validate() + .responseDecodable(completionHandler: getResponseHandler(mapsInnerErrors: true, completion: completion)) + } +} diff --git a/SolardVPNCommunityCoreiOS/SOLARAPI/Providers/SessionProvider/NodeSessionProviderType.swift b/SolardVPNCommunityCoreiOS/SOLARAPI/Providers/SessionProvider/NodeSessionProviderType.swift new file mode 100644 index 0000000..1e14d9b --- /dev/null +++ b/SolardVPNCommunityCoreiOS/SOLARAPI/Providers/SessionProvider/NodeSessionProviderType.swift @@ -0,0 +1,19 @@ +// +// NodeSessionProviderType.swift +// SOLARAPI +// +// Created by Lika Vorobeva on 26.09.2022. +// + +import Foundation +import Alamofire + +public protocol NodeSessionProviderType { + func createClient( + remoteURL: URL, + address: String, + id: String, + request: StartSessionRequest, + completion: @escaping (Result) -> Void + ) +} diff --git a/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS.xcodeproj/project.pbxproj b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS.xcodeproj/project.pbxproj index 17cdbc5..6cfe3e0 100644 --- a/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS.xcodeproj/project.pbxproj +++ b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS.xcodeproj/project.pbxproj @@ -8,6 +8,35 @@ /* Begin PBXBuildFile section */ 22488A9128E7043200FE29C3 /* String+ArrayConversion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22C3EEA428E4733C007DB01B /* String+ArrayConversion.swift */; }; + 22488A9328E726AA00FE29C3 /* TunnelRouteCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22488A9228E726A900FE29C3 /* TunnelRouteCollection.swift */; }; + 22488A9628E7285500FE29C3 /* PostConnectionRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22488A9528E7285500FE29C3 /* PostConnectionRequest.swift */; }; + 22488A9A28E729E600FE29C3 /* GeneralSettingsStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22488A9928E729E600FE29C3 /* GeneralSettingsStorage.swift */; }; + 22488AA028E72C4E00FE29C3 /* UserDefaultsStorageStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22488A9C28E72C4E00FE29C3 /* UserDefaultsStorageStrategy.swift */; }; + 22488AA128E72C4E00FE29C3 /* SettingsStorageStrategyType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22488A9D28E72C4E00FE29C3 /* SettingsStorageStrategyType.swift */; }; + 22488AA228E72C4E00FE29C3 /* KeychainStorageStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22488A9E28E72C4E00FE29C3 /* KeychainStorageStrategy.swift */; }; + 22488AA328E72C4E00FE29C3 /* StoresWallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22488A9F28E72C4E00FE29C3 /* StoresWallet.swift */; }; + 22488AA628E72CF000FE29C3 /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = 22488AA528E72CF000FE29C3 /* KeychainAccess */; }; + 225A836A28EACE6C00F66619 /* ConnectionModelType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 225A834B28EACE6C00F66619 /* ConnectionModelType.swift */; }; + 225A836B28EACE6C00F66619 /* ConnectionModelError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 225A834C28EACE6C00F66619 /* ConnectionModelError.swift */; }; + 225A836C28EACE6C00F66619 /* ConnectionNodeModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 225A834F28EACE6C00F66619 /* ConnectionNodeModel.swift */; }; + 225A837E28EACE6C00F66619 /* ConnectionModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 225A836628EACE6C00F66619 /* ConnectionModel.swift */; }; + 225A838028EACFD900F66619 /* Node.swift in Sources */ = {isa = PBXBuildFile; fileRef = 225A837F28EACFD900F66619 /* Node.swift */; }; + 225A838828EAE01400F66619 /* SessionsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 225A838228EAE01400F66619 /* SessionsService.swift */; }; + 225A838928EAE01400F66619 /* SessionServiceError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 225A838328EAE01400F66619 /* SessionServiceError.swift */; }; + 225A838A28EAE01400F66619 /* SessionsServiceType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 225A838428EAE01400F66619 /* SessionsServiceType.swift */; }; + 225A838B28EAE01400F66619 /* SubscriptionsServiceType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 225A838628EAE01400F66619 /* SubscriptionsServiceType.swift */; }; + 225A838C28EAE01400F66619 /* SubscriptionsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 225A838728EAE01400F66619 /* SubscriptionsService.swift */; }; + 225A839428EAE0A600F66619 /* StartSessionResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 225A838F28EAE0A600F66619 /* StartSessionResponse.swift */; }; + 225A839528EAE0A600F66619 /* StartSessionRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 225A839028EAE0A600F66619 /* StartSessionRequest.swift */; }; + 225A839628EAE0A600F66619 /* NodeSessionProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 225A839128EAE0A600F66619 /* NodeSessionProvider.swift */; }; + 225A839728EAE0A600F66619 /* NodeSessionAPITarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 225A839228EAE0A600F66619 /* NodeSessionAPITarget.swift */; }; + 225A839828EAE0A600F66619 /* NodeSessionProviderType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 225A839328EAE0A600F66619 /* NodeSessionProviderType.swift */; }; + 225A839A28EAE42300F66619 /* StoresConnectInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 225A839928EAE42300F66619 /* StoresConnectInfo.swift */; }; + 225A839D28EAE54400F66619 /* SecurityService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 225A839C28EAE54400F66619 /* SecurityService.swift */; }; + 225A83A028EAE5D200F66619 /* SwiftKeychainWrapper in Frameworks */ = {isa = PBXBuildFile; productRef = 225A839F28EAE5D200F66619 /* SwiftKeychainWrapper */; }; + 225A83A228EB06BF00F66619 /* NodesProviderType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 225A83A128EB06BF00F66619 /* NodesProviderType.swift */; }; + 225A83A328EB14B200F66619 /* PacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22C3EF0028E6FD95007DB01B /* PacketTunnelProvider.swift */; }; + 225A83A628EC81E500F66619 /* WebSocketDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 225A83A528EC81E500F66619 /* WebSocketDelegate.swift */; }; 22C3EE9028E4638E007DB01B /* WireGuardNetworkExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 22C3EE8728E4638E007DB01B /* WireGuardNetworkExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 22C3EE9728E463D6007DB01B /* WireGuardKit in Frameworks */ = {isa = PBXBuildFile; productRef = 22C3EE9628E463D6007DB01B /* WireGuardKit */; }; 22C3EE9A28E464A1007DB01B /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = 22C3EE9928E464A1007DB01B /* Alamofire */; }; @@ -46,7 +75,6 @@ 22C3EEF928E48F0B007DB01B /* DNSServerType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22C3EEF828E48F0A007DB01B /* DNSServerType.swift */; }; 22C3EEFB28E48F95007DB01B /* NotificationToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22C3EEFA28E48F94007DB01B /* NotificationToken.swift */; }; 22C3EEFD28E6FC2C007DB01B /* WireGuardKit in Frameworks */ = {isa = PBXBuildFile; productRef = 22C3EE6928E460B2007DB01B /* WireGuardKit */; }; - 22C3EF0328E6FD95007DB01B /* PacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22C3EF0028E6FD95007DB01B /* PacketTunnelProvider.swift */; }; 22C3EF0628E6FDDB007DB01B /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 22C3EF0528E6FDDB007DB01B /* NetworkExtension.framework */; }; 22C3EF0728E6FDF8007DB01B /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 22C3EF0528E6FDDB007DB01B /* NetworkExtension.framework */; }; 923C371428E5B898003CFC03 /* CommonContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 923C371328E5B898003CFC03 /* CommonContext.swift */; }; @@ -81,7 +109,6 @@ 92D6B44D28E438EF004CF9DF /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = 92D6B44C28E438EF004CF9DF /* Alamofire */; }; 92D6B44F28E45EA0004CF9DF /* NodesAPITarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92D6B44E28E45EA0004CF9DF /* NodesAPITarget.swift */; }; 92D6B45228E45F03004CF9DF /* NodesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92D6B45128E45F03004CF9DF /* NodesProvider.swift */; }; - 92D6B45428E45F15004CF9DF /* NodesProviderType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92D6B45328E45F15004CF9DF /* NodesProviderType.swift */; }; 92D6B45628E4609E004CF9DF /* APITarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92D6B45528E4609E004CF9DF /* APITarget.swift */; }; 92D6B45828E46118004CF9DF /* SOLARAPIProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92D6B45728E46118004CF9DF /* SOLARAPIProvider.swift */; }; 92D6B45A28E46283004CF9DF /* NetworkError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92D6B45928E46283004CF9DF /* NetworkError.swift */; }; @@ -147,6 +174,32 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 22488A9228E726A900FE29C3 /* TunnelRouteCollection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelRouteCollection.swift; sourceTree = ""; }; + 22488A9528E7285500FE29C3 /* PostConnectionRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostConnectionRequest.swift; sourceTree = ""; }; + 22488A9928E729E600FE29C3 /* GeneralSettingsStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneralSettingsStorage.swift; sourceTree = ""; }; + 22488A9C28E72C4E00FE29C3 /* UserDefaultsStorageStrategy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserDefaultsStorageStrategy.swift; sourceTree = ""; }; + 22488A9D28E72C4E00FE29C3 /* SettingsStorageStrategyType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsStorageStrategyType.swift; sourceTree = ""; }; + 22488A9E28E72C4E00FE29C3 /* KeychainStorageStrategy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeychainStorageStrategy.swift; sourceTree = ""; }; + 22488A9F28E72C4E00FE29C3 /* StoresWallet.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StoresWallet.swift; sourceTree = ""; }; + 225A834B28EACE6C00F66619 /* ConnectionModelType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConnectionModelType.swift; sourceTree = ""; }; + 225A834C28EACE6C00F66619 /* ConnectionModelError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConnectionModelError.swift; sourceTree = ""; }; + 225A834F28EACE6C00F66619 /* ConnectionNodeModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConnectionNodeModel.swift; sourceTree = ""; }; + 225A836628EACE6C00F66619 /* ConnectionModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConnectionModel.swift; sourceTree = ""; }; + 225A837F28EACFD900F66619 /* Node.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Node.swift; sourceTree = ""; }; + 225A838228EAE01400F66619 /* SessionsService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SessionsService.swift; sourceTree = ""; }; + 225A838328EAE01400F66619 /* SessionServiceError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SessionServiceError.swift; sourceTree = ""; }; + 225A838428EAE01400F66619 /* SessionsServiceType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SessionsServiceType.swift; sourceTree = ""; }; + 225A838628EAE01400F66619 /* SubscriptionsServiceType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscriptionsServiceType.swift; sourceTree = ""; }; + 225A838728EAE01400F66619 /* SubscriptionsService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscriptionsService.swift; sourceTree = ""; }; + 225A838F28EAE0A600F66619 /* StartSessionResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StartSessionResponse.swift; sourceTree = ""; }; + 225A839028EAE0A600F66619 /* StartSessionRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StartSessionRequest.swift; sourceTree = ""; }; + 225A839128EAE0A600F66619 /* NodeSessionProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NodeSessionProvider.swift; sourceTree = ""; }; + 225A839228EAE0A600F66619 /* NodeSessionAPITarget.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NodeSessionAPITarget.swift; sourceTree = ""; }; + 225A839328EAE0A600F66619 /* NodeSessionProviderType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NodeSessionProviderType.swift; sourceTree = ""; }; + 225A839928EAE42300F66619 /* StoresConnectInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoresConnectInfo.swift; sourceTree = ""; }; + 225A839C28EAE54400F66619 /* SecurityService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecurityService.swift; sourceTree = ""; }; + 225A83A128EB06BF00F66619 /* NodesProviderType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NodesProviderType.swift; sourceTree = ""; }; + 225A83A528EC81E500F66619 /* WebSocketDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebSocketDelegate.swift; sourceTree = ""; }; 22C3EE8728E4638E007DB01B /* WireGuardNetworkExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = WireGuardNetworkExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 22C3EE9B28E467E4007DB01B /* SOLARdVPNCommunityCoreiOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SOLARdVPNCommunityCoreiOS.entitlements; sourceTree = ""; }; 22C3EE9D28E4733C007DB01B /* Config.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Config.swift; sourceTree = ""; }; @@ -210,7 +263,6 @@ 92D6B44528E34D7C004CF9DF /* APIRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIRequest.swift; sourceTree = ""; }; 92D6B44E28E45EA0004CF9DF /* NodesAPITarget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodesAPITarget.swift; sourceTree = ""; }; 92D6B45128E45F03004CF9DF /* NodesProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodesProvider.swift; sourceTree = ""; }; - 92D6B45328E45F15004CF9DF /* NodesProviderType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodesProviderType.swift; sourceTree = ""; }; 92D6B45528E4609E004CF9DF /* APITarget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APITarget.swift; sourceTree = ""; }; 92D6B45728E46118004CF9DF /* SOLARAPIProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SOLARAPIProvider.swift; sourceTree = ""; }; 92D6B45928E46283004CF9DF /* NetworkError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkError.swift; sourceTree = ""; }; @@ -242,6 +294,8 @@ files = ( 22C3EEFD28E6FC2C007DB01B /* WireGuardKit in Frameworks */, 92D6B44D28E438EF004CF9DF /* Alamofire in Frameworks */, + 225A83A028EAE5D200F66619 /* SwiftKeychainWrapper in Frameworks */, + 22488AA628E72CF000FE29C3 /* KeychainAccess in Frameworks */, 22C3EE9A28E464A1007DB01B /* Alamofire in Frameworks */, 22C3EEB128E473E5007DB01B /* SwiftyBeaver in Frameworks */, 22C3EEF128E48E2F007DB01B /* SentinelWallet in Frameworks */, @@ -262,6 +316,112 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 22488A9728E7297000FE29C3 /* Storage */ = { + isa = PBXGroup; + children = ( + 22488A9828E729E600FE29C3 /* GeneralSettingsStorage */, + 22488A9B28E72C4E00FE29C3 /* StorageStrategy */, + 22C3EEF328E48EBE007DB01B /* StoresDNSServers.swift */, + 22488A9F28E72C4E00FE29C3 /* StoresWallet.swift */, + 225A839928EAE42300F66619 /* StoresConnectInfo.swift */, + ); + path = Storage; + sourceTree = ""; + }; + 22488A9828E729E600FE29C3 /* GeneralSettingsStorage */ = { + isa = PBXGroup; + children = ( + 22488A9928E729E600FE29C3 /* GeneralSettingsStorage.swift */, + ); + path = GeneralSettingsStorage; + sourceTree = ""; + }; + 22488A9B28E72C4E00FE29C3 /* StorageStrategy */ = { + isa = PBXGroup; + children = ( + 22488A9C28E72C4E00FE29C3 /* UserDefaultsStorageStrategy.swift */, + 22488A9D28E72C4E00FE29C3 /* SettingsStorageStrategyType.swift */, + 22488A9E28E72C4E00FE29C3 /* KeychainStorageStrategy.swift */, + ); + path = StorageStrategy; + sourceTree = ""; + }; + 225A834628EACE6C00F66619 /* Connection */ = { + isa = PBXGroup; + children = ( + 225A834928EACE6C00F66619 /* Models */, + 225A834F28EACE6C00F66619 /* ConnectionNodeModel.swift */, + 225A836628EACE6C00F66619 /* ConnectionModel.swift */, + ); + path = Connection; + sourceTree = ""; + }; + 225A834928EACE6C00F66619 /* Models */ = { + isa = PBXGroup; + children = ( + 225A834B28EACE6C00F66619 /* ConnectionModelType.swift */, + 225A834C28EACE6C00F66619 /* ConnectionModelError.swift */, + ); + path = Models; + sourceTree = ""; + }; + 225A838128EAE01400F66619 /* SessionsService */ = { + isa = PBXGroup; + children = ( + 225A838228EAE01400F66619 /* SessionsService.swift */, + 225A838328EAE01400F66619 /* SessionServiceError.swift */, + 225A838428EAE01400F66619 /* SessionsServiceType.swift */, + ); + path = SessionsService; + sourceTree = ""; + }; + 225A838528EAE01400F66619 /* SubscriptionsService */ = { + isa = PBXGroup; + children = ( + 225A838628EAE01400F66619 /* SubscriptionsServiceType.swift */, + 225A838728EAE01400F66619 /* SubscriptionsService.swift */, + ); + path = SubscriptionsService; + sourceTree = ""; + }; + 225A838D28EAE0A600F66619 /* SessionProvider */ = { + isa = PBXGroup; + children = ( + 225A838E28EAE0A600F66619 /* Models */, + 225A839128EAE0A600F66619 /* NodeSessionProvider.swift */, + 225A839228EAE0A600F66619 /* NodeSessionAPITarget.swift */, + 225A839328EAE0A600F66619 /* NodeSessionProviderType.swift */, + ); + path = SessionProvider; + sourceTree = ""; + }; + 225A838E28EAE0A600F66619 /* Models */ = { + isa = PBXGroup; + children = ( + 225A838F28EAE0A600F66619 /* StartSessionResponse.swift */, + 225A839028EAE0A600F66619 /* StartSessionRequest.swift */, + ); + path = Models; + sourceTree = ""; + }; + 225A839B28EAE54400F66619 /* SecurityService */ = { + isa = PBXGroup; + children = ( + 225A839C28EAE54400F66619 /* SecurityService.swift */, + ); + path = SecurityService; + sourceTree = ""; + }; + 225A83A428EC297500F66619 /* Tunnel */ = { + isa = PBXGroup; + children = ( + 225A834628EACE6C00F66619 /* Connection */, + 22488A9228E726A900FE29C3 /* TunnelRouteCollection.swift */, + 22488A9528E7285500FE29C3 /* PostConnectionRequest.swift */, + ); + path = Tunnel; + sourceTree = ""; + }; 22C3EE9C28E4733C007DB01B /* Shared */ = { isa = PBXGroup; children = ( @@ -366,7 +526,6 @@ isa = PBXGroup; children = ( 22C3EEF828E48F0A007DB01B /* DNSServerType.swift */, - 22C3EEF328E48EBE007DB01B /* StoresDNSServers.swift */, ); path = DNSServers; sourceTree = ""; @@ -389,6 +548,7 @@ 92D6B41328E1E133004CF9DF /* DVPNServer.swift */, 92D6B3FE28E19E20004CF9DF /* SceneDelegate.swift */, 92D6B40028E19E20004CF9DF /* ViewController.swift */, + 225A83A528EC81E500F66619 /* WebSocketDelegate.swift */, ); path = Root; sourceTree = ""; @@ -410,6 +570,7 @@ 92D6B46E28E47E2E004CF9DF /* NodesService.swift */, 923C372928EAFAA5003CFC03 /* NodesServiceError.swift */, 92D6B47028E47E71004CF9DF /* NodesServiceType.swift */, + 225A837F28EACFD900F66619 /* Node.swift */, ); path = NodesService; sourceTree = ""; @@ -427,6 +588,7 @@ isa = PBXGroup; children = ( 923C372428E72FF0003CFC03 /* Nodes */, + 225A83A428EC297500F66619 /* Tunnel */, ); path = RouteCollections; sourceTree = ""; @@ -543,6 +705,7 @@ 92D6B44A28E4360D004CF9DF /* Providers */ = { isa = PBXGroup; children = ( + 225A838D28EAE0A600F66619 /* SessionProvider */, 92D6B45028E45EF1004CF9DF /* NodesProvider */, ); path = Providers; @@ -554,7 +717,7 @@ 92D6B45B28E46350004CF9DF /* Models */, 92D6B44E28E45EA0004CF9DF /* NodesAPITarget.swift */, 92D6B45128E45F03004CF9DF /* NodesProvider.swift */, - 92D6B45328E45F15004CF9DF /* NodesProviderType.swift */, + 225A83A128EB06BF00F66619 /* NodesProviderType.swift */, ); path = NodesProvider; sourceTree = ""; @@ -599,8 +762,12 @@ 92D6B46D28E47DD2004CF9DF /* Services */ = { isa = PBXGroup; children = ( - 22C3EF0A28E6FEF0007DB01B /* NodesService */, 22C3EEF228E48EBE007DB01B /* DNSServers */, + 22C3EF0A28E6FEF0007DB01B /* NodesService */, + 225A839B28EAE54400F66619 /* SecurityService */, + 225A838128EAE01400F66619 /* SessionsService */, + 22488A9728E7297000FE29C3 /* Storage */, + 225A838528EAE01400F66619 /* SubscriptionsService */, 22C3EEBB28E48B51007DB01B /* Tunnel */, ); path = Services; @@ -693,6 +860,8 @@ 22C3EE9928E464A1007DB01B /* Alamofire */, 22C3EEB028E473E5007DB01B /* SwiftyBeaver */, 22C3EEF028E48E2F007DB01B /* SentinelWallet */, + 22488AA528E72CF000FE29C3 /* KeychainAccess */, + 225A839F28EAE5D200F66619 /* SwiftKeychainWrapper */, ); productName = SolardVPNCommunityCoreiOS; productReference = 92D6B3F928E19E20004CF9DF /* SOLARdVPNCommunityCoreiOS.app */; @@ -759,6 +928,8 @@ 22C3EE9828E464A1007DB01B /* XCRemoteSwiftPackageReference "Alamofire" */, 22C3EEAF28E473E5007DB01B /* XCRemoteSwiftPackageReference "SwiftyBeaver" */, 22C3EEEF28E48E2F007DB01B /* XCRemoteSwiftPackageReference "sentinel-wallet-sdk-ios" */, + 22488AA428E72CF000FE29C3 /* XCRemoteSwiftPackageReference "KeychainAccess" */, + 225A839E28EAE5D200F66619 /* XCRemoteSwiftPackageReference "SwiftKeychainWrapper" */, ); productRefGroup = 92D6B3FA28E19E20004CF9DF /* Products */; projectDirPath = ""; @@ -810,6 +981,7 @@ 22C3EEB328E4748E007DB01B /* NETunnelProviderProtocol+Extension.swift in Sources */, 22C3EEB528E4748E007DB01B /* Keychain.swift in Sources */, 22C3EEB828E4748E007DB01B /* ConfigurationParseError.swift in Sources */, + 225A83A328EB14B200F66619 /* PacketTunnelProvider.swift in Sources */, 22C3EEB728E4748E007DB01B /* TunnelConfiguration+WireGuardConfig.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -821,27 +993,42 @@ 92D6B40128E19E20004CF9DF /* ViewController.swift in Sources */, 22C3EEED28E48D9A007DB01B /* SentinelNode+Ext.swift in Sources */, 923C372628EAE93E003CFC03 /* Encoder.swift in Sources */, + 225A838028EACFD900F66619 /* Node.swift in Sources */, 92D6B41828E2DC85004CF9DF /* NodesRouteCollection.swift in Sources */, 923C371428E5B898003CFC03 /* CommonContext.swift in Sources */, 923C372C28EB08BB003CFC03 /* ContinentDecoder.swift in Sources */, 923C373128EB0CB2003CFC03 /* CountryExtra.swift in Sources */, 92D6B3FD28E19E20004CF9DF /* AppDelegate.swift in Sources */, 923C371C28E5DA05003CFC03 /* OrderType.swift in Sources */, + 225A838C28EAE01400F66619 /* SubscriptionsService.swift in Sources */, 92D6B46F28E47E2E004CF9DF /* NodesService.swift in Sources */, 22C3EEA728E4733C007DB01B /* Config.swift in Sources */, 22C3EEE528E48D9A007DB01B /* NEVPNManager+Ext.swift in Sources */, + 22488AA028E72C4E00FE29C3 /* UserDefaultsStorageStrategy.swift in Sources */, 22C3EECA28E48B52007DB01B /* TunnelManager.swift in Sources */, 22C3EED028E48B52007DB01B /* TunnelModel.swift in Sources */, 22C3EECC28E48B52007DB01B /* TunnelChanges.swift in Sources */, + 225A837E28EACE6C00F66619 /* ConnectionModel.swift in Sources */, 22C3EEAB28E4733C007DB01B /* TunnelConfiguration+WireGuardConfig.swift in Sources */, 22C3EECB28E48B52007DB01B /* TunnelManagerType.swift in Sources */, + 225A838828EAE01400F66619 /* SessionsService.swift in Sources */, + 225A839A28EAE42300F66619 /* StoresConnectInfo.swift in Sources */, + 225A838A28EAE01400F66619 /* SessionsServiceType.swift in Sources */, + 22488A9328E726AA00FE29C3 /* TunnelRouteCollection.swift in Sources */, 22488A9128E7043200FE29C3 /* String+ArrayConversion.swift in Sources */, - 22C3EF0328E6FD95007DB01B /* PacketTunnelProvider.swift in Sources */, + 22488AA128E72C4E00FE29C3 /* SettingsStorageStrategyType.swift in Sources */, + 225A838928EAE01400F66619 /* SessionServiceError.swift in Sources */, 22C3EEF928E48F0B007DB01B /* DNSServerType.swift in Sources */, + 225A836A28EACE6C00F66619 /* ConnectionModelType.swift in Sources */, + 225A838B28EAE01400F66619 /* SubscriptionsServiceType.swift in Sources */, 22C3EEAD28E4733C007DB01B /* ConfigurationParseError.swift in Sources */, + 225A83A628EC81E500F66619 /* WebSocketDelegate.swift in Sources */, 22C3EED228E48B52007DB01B /* TunnelContainer.swift in Sources */, + 22488A9A28E729E600FE29C3 /* GeneralSettingsStorage.swift in Sources */, 22C3EEFB28E48F95007DB01B /* NotificationToken.swift in Sources */, + 225A836B28EACE6C00F66619 /* ConnectionModelError.swift in Sources */, 22C3EED428E48B52007DB01B /* TunnelsService.swift in Sources */, + 225A839D28EAE54400F66619 /* SecurityService.swift in Sources */, 22C3EEE628E48D9A007DB01B /* TunnelConfiguration+Ext.swift in Sources */, 22C3EEE828E48D9A007DB01B /* Serializer.swift in Sources */, 22C3EEE728E48D9A007DB01B /* NEVPNStatus+Ext.swift in Sources */, @@ -856,14 +1043,18 @@ 923C371628E5B9DE003CFC03 /* ContextBuilder.swift in Sources */, 22C3EECD28E48B52007DB01B /* TunnelInterfaceModel.swift in Sources */, 22C3EEF628E48EBF007DB01B /* StoresDNSServers.swift in Sources */, + 22488A9628E7285500FE29C3 /* PostConnectionRequest.swift in Sources */, 92D6B3FF28E19E20004CF9DF /* SceneDelegate.swift in Sources */, 22C3EED328E48B52007DB01B /* TunnelsServiceStatusDelegate.swift in Sources */, 22C3EECF28E48B52007DB01B /* TunnelSavingError.swift in Sources */, + 22488AA228E72C4E00FE29C3 /* KeychainStorageStrategy.swift in Sources */, 22C3EEE428E48D9A007DB01B /* NETunnelProviderManager+Ext.swift in Sources */, 22C3EEAA28E4733C007DB01B /* NETunnelProviderProtocol+Extension.swift in Sources */, + 22488AA328E72C4E00FE29C3 /* StoresWallet.swift in Sources */, 22C3EED128E48B52007DB01B /* TunnelStatus.swift in Sources */, 923C373328EB150A003CFC03 /* GetContinentResponse.swift in Sources */, 923C372A28EAFAA5003CFC03 /* NodesServiceError.swift in Sources */, + 225A836C28EACE6C00F66619 /* ConnectionNodeModel.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -874,19 +1065,24 @@ 92D6B44428E34A9B004CF9DF /* AnyEncodable.swift in Sources */, 92D6B44F28E45EA0004CF9DF /* NodesAPITarget.swift in Sources */, 92D6B45D28E4636B004CF9DF /* GetNodesRequest.swift in Sources */, + 225A839628EAE0A600F66619 /* NodeSessionProvider.swift in Sources */, + 225A839528EAE0A600F66619 /* StartSessionRequest.swift in Sources */, 92D6B46628E47311004CF9DF /* PageResponse.swift in Sources */, 92D6B46128E463EF004CF9DF /* NodeStatusType.swift in Sources */, - 92D6B45428E45F15004CF9DF /* NodesProviderType.swift in Sources */, 92D6B46328E47240004CF9DF /* Node.swift in Sources */, + 225A839828EAE0A600F66619 /* NodeSessionProviderType.swift in Sources */, 92D6B45F28E4639D004CF9DF /* OrderType.swift in Sources */, 92D6B45228E45F03004CF9DF /* NodesProvider.swift in Sources */, 923C372828EAF0E8003CFC03 /* Country.swift in Sources */, + 225A839428EAE0A600F66619 /* StartSessionResponse.swift in Sources */, 92D6B44628E34D7C004CF9DF /* APIRequest.swift in Sources */, 92D6B42828E32900004CF9DF /* SOLARAPI.docc in Sources */, 92D6B45A28E46283004CF9DF /* NetworkError.swift in Sources */, 923C372028E71D19003CFC03 /* PostNodesByAddressRequest.swift in Sources */, + 225A839728EAE0A600F66619 /* NodeSessionAPITarget.swift in Sources */, 92D6B46C28E47519004CF9DF /* Encodable+Ext.swift in Sources */, 92D6B45628E4609E004CF9DF /* APITarget.swift in Sources */, + 225A83A228EB06BF00F66619 /* NodesProviderType.swift in Sources */, 92D6B45828E46118004CF9DF /* SOLARAPIProvider.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -971,7 +1167,7 @@ INFOPLIST_FILE = WireGuardNetworkExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = WireGuardNetworkExtension; INFOPLIST_KEY_NSHumanReadableCopyright = ""; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1000,7 +1196,7 @@ INFOPLIST_FILE = WireGuardNetworkExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = WireGuardNetworkExtension; INFOPLIST_KEY_NSHumanReadableCopyright = ""; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1146,8 +1342,8 @@ INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; INFOPLIST_KEY_UIMainStoryboardFile = Main; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1155,9 +1351,12 @@ MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = "ee.solarlabs.community-core.ios"; PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = 1; }; name = Debug; }; @@ -1177,8 +1376,8 @@ INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; INFOPLIST_KEY_UIMainStoryboardFile = Main; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1186,9 +1385,12 @@ MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = "ee.solarlabs.community-core.ios"; PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = 1; }; name = Release; }; @@ -1305,6 +1507,22 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ + 22488AA428E72CF000FE29C3 /* XCRemoteSwiftPackageReference "KeychainAccess" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/kishikawakatsumi/KeychainAccess.git"; + requirement = { + branch = master; + kind = branch; + }; + }; + 225A839E28EAE5D200F66619 /* XCRemoteSwiftPackageReference "SwiftKeychainWrapper" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/jrendel/SwiftKeychainWrapper"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 4.0.0; + }; + }; 22C3EE6828E460B2007DB01B /* XCRemoteSwiftPackageReference "wireguard-apple" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://git.zx2c4.com/wireguard-apple"; @@ -1356,6 +1574,16 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ + 22488AA528E72CF000FE29C3 /* KeychainAccess */ = { + isa = XCSwiftPackageProductDependency; + package = 22488AA428E72CF000FE29C3 /* XCRemoteSwiftPackageReference "KeychainAccess" */; + productName = KeychainAccess; + }; + 225A839F28EAE5D200F66619 /* SwiftKeychainWrapper */ = { + isa = XCSwiftPackageProductDependency; + package = 225A839E28EAE5D200F66619 /* XCRemoteSwiftPackageReference "SwiftKeychainWrapper" */; + productName = SwiftKeychainWrapper; + }; 22C3EE6928E460B2007DB01B /* WireGuardKit */ = { isa = XCSwiftPackageProductDependency; package = 22C3EE6828E460B2007DB01B /* XCRemoteSwiftPackageReference "wireguard-apple" */; diff --git a/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Common/Context/CommonContext.swift b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Common/Context/CommonContext.swift index ddad30b..791feeb 100644 --- a/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Common/Context/CommonContext.swift +++ b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Common/Context/CommonContext.swift @@ -6,28 +6,110 @@ // import Foundation +import SentinelWallet import SOLARAPI protocol NoContext {} final class CommonContext { - // Providers - let nodesProvider: NodesProviderType + typealias Storage = StoresConnectInfo & StoresWallet & StoresDNSServers + let storage: Storage + // Providers + let nodesProvider: SOLARAPI.NodesProviderType + // Services + let securityService: SecurityService let nodesService: NodesServiceType + let walletService: WalletService + let subscriptionsService: SubscriptionsServiceType + let sessionsService: SessionsServiceType + + let tunnelManager: TunnelManagerType + init( - nodesProvider: NodesProviderType, - nodesService: NodesServiceType + storage: Storage, + nodesProvider: SOLARAPI.NodesProviderType, + securityService: SecurityService, + walletService: WalletService, + nodesService: NodesServiceType, + subscriptionsService: SubscriptionsServiceType, + sessionsService: SessionsServiceType, + tunnelManager: TunnelManagerType ) { + self.storage = storage self.nodesProvider = nodesProvider + self.securityService = securityService + self.walletService = walletService self.nodesService = nodesService + self.subscriptionsService = subscriptionsService + self.sessionsService = sessionsService + self.tunnelManager = tunnelManager } } -protocol HasNodesProvider { var nodesProvider: NodesProviderType { get } } +protocol HasWalletService { + var walletService: WalletService { get } + + func updateWalletContext() + func resetWalletContext() +} + +extension CommonContext: HasWalletService { + func updateWalletContext() { + let walletAddress = storage.walletAddress + guard !walletAddress.isEmpty else { return } + walletService.manage(address: walletAddress) + } + + func resetWalletContext() { + let mnemonics = securityService.generateMnemonics().components(separatedBy: " ") + switch securityService.restore(from: mnemonics) { + case .failure(let error): + fatalError("failed to generate wallet due to \(error), terminate") + case .success(let address): + saveMnemonicsIfNeeded(for: address, mnemonics: mnemonics) + storage.set(wallet: address) + updateWalletContext() + } + } + + private func saveMnemonicsIfNeeded(for account: String, mnemonics: [String]) { + guard !securityService.mnemonicsExists(for: account) else { return } + if !self.securityService.save(mnemonics: mnemonics, for: account) { + log.error("Failed to save mnemonics info") + } + } +} + +protocol HasSubscriptionsService { var subscriptionsService: SubscriptionsServiceType { get } } +extension CommonContext: HasSubscriptionsService {} + +protocol HasNodesProvider { var nodesProvider: SOLARAPI.NodesProviderType { get } } extension CommonContext: HasNodesProvider {} protocol HasNodesService { var nodesService: NodesServiceType { get } } extension CommonContext: HasNodesService {} + +protocol HasTunnelManager { var tunnelManager: TunnelManagerType { get } } +extension CommonContext: HasTunnelManager {} + +protocol HasSessionsService { var sessionsService: SessionsServiceType { get } } +extension CommonContext: HasSessionsService {} + +// MARK: - Storages + +protocol HasConnectionInfoStorage { var connectionInfoStorage: StoresConnectInfo { get } } +extension CommonContext: HasConnectionInfoStorage { + var connectionInfoStorage: StoresConnectInfo { + storage as StoresConnectInfo + } +} + +protocol HasWalletStorage { var walletStorage: StoresWallet { get } } +extension CommonContext: HasWalletStorage { + var walletStorage: StoresWallet { + storage as StoresWallet + } +} diff --git a/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Common/Context/ContextBuilder.swift b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Common/Context/ContextBuilder.swift index 96bf911..6061afe 100644 --- a/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Common/Context/ContextBuilder.swift +++ b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Common/Context/ContextBuilder.swift @@ -6,18 +6,82 @@ // import Foundation +import SentinelWallet import SOLARAPI /// This class should configure all required services and inject them into a Context final class ContextBuilder { func buildContext() -> CommonContext { - let nodesProvider = NodesProvider(configuration: .init(baseURL: ClientConstants.backendURL)) + let generalSettingsStorage = GeneralSettingsStorage() + let nodesProvider = NodesProvider(configuration: .init(baseURL: ClientConstants.backendURL)) let nodesService = NodesService(nodesProvider: nodesProvider) + + let securityService = SecurityService() + let subscriptionsProvider = SubscriptionsProvider(configuration: ApplicationConfiguration.shared) + let sessionProvider = NodeSessionProvider() + let walletService = buildWalletService(storage: generalSettingsStorage, securityService: securityService) + + let sessionsService = SessionsService( + sessionProvider: sessionProvider, + subscriptionsProvider: subscriptionsProvider, + walletService: walletService + ) + let subscriptionsService = SubscriptionsService( + subscriptionsProvider: subscriptionsProvider, + walletService: walletService + ) + + let tunnelManager = TunnelManager(storage: generalSettingsStorage) return CommonContext( + storage: generalSettingsStorage, nodesProvider: nodesProvider, - nodesService: nodesService + securityService: securityService, + walletService: walletService, + nodesService: nodesService, + subscriptionsService: subscriptionsService, + sessionsService: sessionsService, + tunnelManager: tunnelManager ) } + + func buildWalletService( + storage: StoresWallet, + securityService: SecurityService + ) -> WalletService { + let walletAddress = storage.walletAddress + let grpc = ApplicationConfiguration.shared + guard !walletAddress.isEmpty else { + let mnemonic = securityService.generateMnemonics().components(separatedBy: " ") + switch securityService.restore(from: mnemonic) { + case .failure(let error): + fatalError("failed to generate wallet due to \(error), terminate") + case .success(let address): + saveMnemonicsIfNeeded(for: address, mnemonics: mnemonic, securityService: securityService) + storage.set(wallet: address) + return .init( + for: address, + configuration: grpc, + securityService: securityService + ) + } + } + return WalletService( + for: walletAddress, + configuration: grpc, + securityService: securityService + ) + } + + private func saveMnemonicsIfNeeded( + for account: String, + mnemonics: [String], + securityService: SecurityService + ) { + guard !securityService.mnemonicsExists(for: account) else { return } + if !securityService.save(mnemonics: mnemonics, for: account) { + log.error("Failed to save mnemonics info") + } + } } diff --git a/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Common/Extensions/Wallet/SentinelNode+Ext.swift b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Common/Extensions/Wallet/SentinelNode+Ext.swift index 40d5207..547ce76 100644 --- a/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Common/Extensions/Wallet/SentinelNode+Ext.swift +++ b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Common/Extensions/Wallet/SentinelNode+Ext.swift @@ -8,7 +8,7 @@ import SentinelWallet extension SentinelNode { - func set(node: Node) -> SentinelNode { + func set(node: SentinelWallet.Node) -> SentinelNode { return .init( address: self.address, provider: self.provider, diff --git a/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Common/Utilities/ClientConstants.swift b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Common/Utilities/ClientConstants.swift index ee43f16..26a89cf 100644 --- a/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Common/Utilities/ClientConstants.swift +++ b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Common/Utilities/ClientConstants.swift @@ -6,12 +6,25 @@ // import Foundation +import SentinelWallet enum ClientConstants { static let host = "localhost" static let port = 3876 + static let defaultLCDHostString = "lcd-sentinel.dvpn.solar" + static let defaultLCDPort = 993 + static let apiPath = "api" static let backendURL = URL(string: "https://BACKEND")! } + +final class ApplicationConfiguration: ClientConnectionConfigurationType { + private(set) static var shared = ApplicationConfiguration() + + var grpcMirror: ClientConnectionConfiguration = .init( + host: ClientConstants.defaultLCDHostString, + port: ClientConstants.defaultLCDPort + ) +} diff --git a/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Info.plist b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Info.plist index 0026520..1597a8d 100644 --- a/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Info.plist +++ b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Info.plist @@ -2,13 +2,13 @@ + App GroupID + group.ee.solarlabs.community-core.ios NSAppTransportSecurity NSAllowsArbitraryLoads - App GroupID - group.ee.solarlabs.community-core.ios UIApplicationSceneManifest UIApplicationSupportsMultipleScenes diff --git a/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Root/DVPNServer.swift b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Root/DVPNServer.swift index 5627dc7..f33cb34 100644 --- a/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Root/DVPNServer.swift +++ b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Root/DVPNServer.swift @@ -9,11 +9,13 @@ import Vapor class DVPNServer: ObservableObject { let app: Application + private var currentClientConnection: WebSocket? private let context: CommonContext init(context: CommonContext) { app = Application(.development) self.context = context + Config.setup() configure(app) } @@ -25,6 +27,12 @@ extension DVPNServer { do { let api = app.grouped(.init(stringLiteral: ClientConstants.apiPath)) try api.register(collection: NodesRouteCollection(context: context)) + try api.register(collection: + TunnelRouteCollection( + model: ConnectionModel(context: context), + delegate: self + ) + ) try app.start() } catch { fatalError(error.localizedDescription) @@ -39,5 +47,30 @@ extension DVPNServer { private func configure(_ app: Application) { app.http.server.configuration.hostname = ClientConstants.host app.http.server.configuration.port = ClientConstants.port + + configureConnection() + } + + private func configureConnection() { + app.webSocket("echo") { [weak self] req, client in + client.pingInterval = .seconds(5) + self?.currentClientConnection = client + + client.onClose.whenComplete { _ in + self?.currentClientConnection = nil + } + + client.onText { ws, text in + log.debug(text) + } + } + } +} + +// MARK: - WebSocketDelegate + +extension DVPNServer: WebSocketDelegate { + func send(event: String) { + currentClientConnection?.send(event) } } diff --git a/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Root/RouteCollections/Tunnel/Connection/ConnectionModel.swift b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Root/RouteCollections/Tunnel/Connection/ConnectionModel.swift new file mode 100644 index 0000000..4357010 --- /dev/null +++ b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Root/RouteCollections/Tunnel/Connection/ConnectionModel.swift @@ -0,0 +1,248 @@ +// +// ConnectionModel.swift +// SOLARdVPNCommunityCoreiOS +// +// Created by Lika Vorobeva on 30.09.2022. +// + +import Foundation +import Combine +import SentinelWallet +import GRPC + +// MARK: - ConnectionModelEvent + +enum ConnectionModelEvent { + case error(Error) + case warning(Error) + case info(String) + + case updateTunnelActivity(isActive: Bool) +} + +// MARK: - ConnectionModel + +final class ConnectionModel { + typealias Delegate = ConnectionModelType + typealias Context = ConnectionNodeModel.Context & HasWalletStorage + private let context: Context + + private let eventSubject = PassthroughSubject() + var eventPublisher: AnyPublisher { + eventSubject.eraseToAnyPublisher() + } + + private var cancellables = Set() + + private var isTunnelActive: Bool { + context.tunnelManager.isTunnelActive + } + + private lazy var dvpnModel = ConnectionNodeModel(context: context, delegate: self) + private weak var delegate: Delegate? + + init(context: Context) { + self.context = context + + context.tunnelManager.delegate = self + delegate = dvpnModel + + fetchWalletInfo() + } +} + +// MARK: - ConnectionModelDelegate + +extension ConnectionModel: ConnectionModelDelegate { + func set(isLoading: Bool) { + if !isLoading { stopLoading() } + } +} + +// MARK: - NodeModelDelegate + +extension ConnectionModel: NodeModelDelegate { + func suggestUnsubscribe(from node: Node) { + eventSubject.send(.error(SessionsServiceError.nodeMisconfigured)) + } + + func openPlans(node: Node, resubscribe: Bool) { + eventSubject.send(.error(resubscribe ? ConnectionModelError.noQuotaLeft : .noSubscription)) + } +} + +// MARK: - Connection functions + +extension ConnectionModel { + func setInitData() { + updateConnectionType(forceUpdate: true, disconnect: false) + } + + func refresh() { + updateConnectionType() + } + + /// Should be called each time when we turn toggle to "on" state + func connect() { + delegate?.connect() + } + + /// Should be called each time when we turn toggle to "off" state + func disconnect() { + guard let tunnel = context.tunnelManager.lastTunnel, tunnel.status != .disconnected else { + stopLoading() + return + } + + context.tunnelManager.startDeactivation(of: tunnel) + } + + func cancelSubscriptions(for nodeAddress: String) { + context.subscriptionsService.loadActiveSubscriptions { [weak self] result in + switch result { + case let .success(subscriptions): + let subscriptionsToCancel = subscriptions.filter { $0.node == nodeAddress }.map { $0.id } + + self?.context.subscriptionsService.cancel( + subscriptions: subscriptionsToCancel, + with: nodeAddress + ) { [weak self] result in + switch result { + case let .failure(error): + self?.show(error: error) + case .success: + self?.handleCancellation(address: nodeAddress) + } + } + case let .failure(error): + self?.show(error: error) + } + } + } +} + +// MARK: - Events + +extension ConnectionModel { + internal func show(error: Error) { + log.error(error) + stopLoading() + + eventSubject.send(.updateTunnelActivity(isActive: isTunnelActive)) + eventSubject.send(.error(error)) + } + + func show(warning: Error) { + eventSubject.send(.warning(warning)) + } + + private func stopLoading() { + eventSubject.send(.updateTunnelActivity(isActive: isTunnelActive)) + } + + private func handleCancellation(address: String) { + eventSubject.send(.info(TunnelRouteEvent.subscriptionCanceled.rawValue)) + + stopLoading() + delegate?.refreshNode() + } +} + +// MARK: - Connection type + +extension ConnectionModel { + func updateConnectionType(forceUpdate: Bool = false, disconnect: Bool = true) { + guard forceUpdate else { + delegate?.refreshNode() + return + } + + if disconnect { + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { + self.disconnect() + } + } + } +} + +// MARK: - Wallet + +extension ConnectionModel { + private func fetchWalletInfo() { + context.walletService.fetchAuthorization { [weak self] error in + if let error = error { + if let statusError = error as? GRPC.GRPCStatus, statusError.code == .notFound { + return + } + self?.show(error: error) + } + } + + context.walletService.fetchTendermintNodeInfo { [weak self] result in + switch result { + case .success(let info): + log.debug(info) + case .failure(let error): + self?.show(error: error) + } + } + } +} + +// MARK: - Account info + +extension ConnectionModel { + var address: String { + context.walletStorage.walletAddress + } +} + +// MARK: - TunnelManagerDelegate + +extension ConnectionModel: TunnelManagerDelegate { + func handleTunnelUpdatingStatus() { } + + func handleError(_ error: Error) { + show(error: error) + } + + func handleTunnelReconnection() { } + + func handleTunnelServiceCreation() { } +} + +// MARK: - TunnelsServiceStatusDelegate + +extension ConnectionModel: TunnelsServiceStatusDelegate { + func activationAttemptFailed(for tunnel: TunnelContainer, with error: TunnelActivationError) { + show(error: error) + } + + func activationAttemptSucceeded(for tunnel: TunnelContainer) { + log.debug("\(tunnel.name) is succesfully attempted activation") + + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { + self.stopLoading() + } + } + + func activationFailed(for tunnel: TunnelContainer, with error: TunnelActivationError) { + show(error: error) + } + + func activationSucceeded(for tunnel: TunnelContainer) { + log.debug("\(tunnel.name) is succesfully activated") + + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { + self.stopLoading() + } + } + + func deactivationSucceeded(for tunnel: TunnelContainer) { + log.debug("\(tunnel.name) is succesfully deactivated") + + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { + self.stopLoading() + } + } +} diff --git a/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Root/RouteCollections/Tunnel/Connection/ConnectionNodeModel.swift b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Root/RouteCollections/Tunnel/Connection/ConnectionNodeModel.swift new file mode 100644 index 0000000..31da4f8 --- /dev/null +++ b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Root/RouteCollections/Tunnel/Connection/ConnectionNodeModel.swift @@ -0,0 +1,431 @@ +// +// ConnectionNodeModel.swift +// SOLARdVPNCommunityCoreiOS +// +// Created by Lika Vorobeva on 30.09.2022. +// + +import Foundation +import Combine +import SentinelWallet +import GRPC + +// MARK: - Constants + +private struct Constants { + let timeout: TimeInterval = 15 + let denom = "udvpn" +} + +private let constants = Constants() + +// MARK: - ConnectionNodeModel + +final class ConnectionNodeModel { + typealias Delegate = ConnectionModelDelegate & NodeModelDelegate + typealias Context = HasConnectionInfoStorage & HasSubscriptionsService & HasTunnelManager & HasWalletService + & HasNodesService & HasSessionsService & HasWalletStorage + private let context: Context + + private var cancellables = Set() + private(set) weak var delegate: Delegate? + + private var subscription: SentinelWallet.Subscription? + private var selectedNode: Node? + + private var isTunnelActive: Bool { + context.tunnelManager.isTunnelActive + } + + init(context: Context, delegate: Delegate) { + self.context = context + self.delegate = delegate + + loadData() + } +} + +// MARK: - Connection functions + +extension ConnectionNodeModel: ConnectionModelType { + func refreshNode() { + checkNodeForUpdate() + } + + /// Should be called each time when we turn toggle to "on" state + func connect() { + guard let subscription = subscription else { + guard let selectedNode = selectedNode else { + return + } + + delegate?.openPlans(node: selectedNode, resubscribe: false) + return + } + delegate?.set(isLoading: true) + guard subscription.node == selectedNode?.address else { + loadSubscriptions(reconnect: true, address: subscription.node) + return + } + detectConnectionAndHandle(considerStatus: false, reconnect: true, subscription: subscription) + } +} + +// MARK: - Connection functions + +extension ConnectionNodeModel { + func loadData() { + delegate?.set(isLoading: false) + setSelectedOrDefaultNodeInfo() + } + + /// Refreshes subscriptions. Should be called each time when the app leaves the background state. + func refreshNodeState() { + guard subscription != nil else { return } + refreshSubscriptions() + } + + /// Should be called each time when the view appears + private func checkNodeForUpdate() { + guard let address = context.connectionInfoStorage.lastSelectedNode() else { + log.error("No last selected node") + + return + } + var time: Double = 0 + if isTunnelActive, let selectedNode = selectedNode?.address, selectedNode != address { + context.tunnelManager.startDeactivationOfActiveTunnel() + delegate?.set(isLoading: true) + time = 2 + } + DispatchQueue.main.asyncAfter(deadline: .now() + time) { + self.updateLocation(address: address) + self.refreshSubscriptions() + } + } +} + +extension ConnectionNodeModel { + // MARK: - Default last selected node + private func setSelectedOrDefaultNodeInfo() { + var address = context.connectionInfoStorage.lastSelectedNode() ?? defaultNodeAddress + if address?.isEmpty == true { + address = defaultNodeAddress + } + if let address = address { + setInitialNodeInfo(address: address) + } + } + + private var defaultNodeAddress: String? { + let trustedNode = context.nodesService.nodes.first(where: { $0.isTrusted }) + if let trustedNode = trustedNode { + context.connectionInfoStorage.set(lastSelectedNode: trustedNode.address) + return trustedNode.address + } + + let randomNode = context.nodesService.nodes.randomElement() + if let randomNode = randomNode { + context.connectionInfoStorage.set(lastSelectedNode: randomNode.address) + } + + return randomNode?.address + } + + private func setInitialNodeInfo(address: String) { + updateLocation(address: address) + + loadSubscriptions(address: address) + } + + // MARK: - Subscriprion + + /// Returns false if no quota + private func checkQuotaAndSubscription(hasQuota: Bool) -> Bool { + guard hasQuota, subscription?.isActive ?? false else { + guard let selectedNode = selectedNode else { + return false + } + + delegate?.openPlans(node: selectedNode, resubscribe: false) + delegate?.set(isLoading: false) + return false + } + + return true + } + + private func refreshSubscriptions() { + delegate?.set(isLoading: false) + + guard let selectedAddress = context.connectionInfoStorage.lastSelectedNode() else { + handleConnection(reconnect: false) + return + } + loadSubscriptions(address: selectedAddress) + } + + private func loadSubscriptions(reconnect: Bool = false, address: String) { + context.subscriptionsService.loadActiveSubscriptions { [weak self] result in + switch result { + case let .success(subscriptions): + guard let subscription = subscriptions.last(where: { $0.node == address }) else { + self?.handleConnection(reconnect: false) + return + } + + self?.subscription = subscription + self?.handleConnection(reconnect: false) + + case let .failure(error): + log.error(error) + self?.delegate?.show(error: error) + } + } + } + + private func handleConnection(reconnect: Bool) { + guard let subscription = subscription else { + if context.tunnelManager.startDeactivationOfActiveTunnel() != true { + delegate?.set(isLoading: false) + } + return + } + + if reconnect { + detectConnectionAndHandle(reconnect: reconnect, subscription: subscription) + } else { + update(subscriptionInfo: subscription, askForResubscription: false) + } + } + + private func update( + subscriptionInfo: SentinelWallet.Subscription, + askForResubscription: Bool = true + ) { + context.subscriptionsService.queryQuota(for: subscriptionInfo.id) { [weak self] result in + guard let self = self else { return } + + switch result { + case .failure(let error): + self.delegate?.show(error: error) + + case .success(let quota): + guard self.update(quota: quota, askForResubscription: askForResubscription) else { + return + } + self.delegate?.set(isLoading: false) + } + } + + updateLocation(address: subscriptionInfo.node) + } + + private func connect(to subscription: SentinelWallet.Subscription) { + context.subscriptionsService.queryQuota(for: subscription.id) { [weak self] result in + guard let self = self else { return } + + switch result { + case .failure(let error): + self.delegate?.show(error: error) + + case .success(let quota): + guard self.update(quota: quota, askForResubscription: true) else { + return + } + + self.context.nodesService.getNode(by: subscription.node) { [weak self] result in + switch result { + case .failure(let error): + self?.delegate?.show(error: error) + case .success(let node): + self?.createNewSession(subscription: subscription, nodeURL: node.remoteURL) + } + } + } + } + } + + private func update(quota: Quota, askForResubscription: Bool) -> Bool { + let initialBandwidth = quota.allocated + let bandwidthConsumed = quota.consumed + + let bandwidthLeft = (Int64(initialBandwidth) ?? 0) - (Int64(bandwidthConsumed) ?? 0) + + return askForResubscription ? checkQuotaAndSubscription(hasQuota: bandwidthLeft != 0) : true + } + + private func updateLocation(address: String) { + context.nodesService.getNode(by: address) { [weak self] result in + switch result { + case let .failure(error): + guard self?.subscription != nil else { return } + log.error(error) + self?.delegate?.show(error: ConnectionModelError.nodeIsOffline) + case let .success(node): + self?.selectedNode = node + } + } + } + + private func createNewSession(subscription: SentinelWallet.Subscription, nodeURL: String) { + context.walletService.fetchBalance { [weak self] result in + guard let self = self else { return } + switch result { + case .failure(let error): + log.debug("Failed to fetch a balance due to \(error). Try to start a session anyway.") + self.startSession(on: subscription, nodeURL: nodeURL) + + case .success(let balances): + guard balances + .contains( + where: { $0.denom == constants.denom && Int($0.amount) ?? 0 >= self.context.walletService.fee } + ) else { + self.delegate?.show(warning: WalletServiceError.notEnoughTokens) + self.delegate?.set(isLoading: false) + return + } + self.startSession(on: subscription, nodeURL: nodeURL) + } + } + } + + private func startSession(on subscription: SentinelWallet.Subscription, nodeURL: String) { + guard let nodeAddress = context.connectionInfoStorage.lastSelectedNode() else { + return + } + + context.sessionsService.startSession(on: subscription.id, node: nodeAddress) { [weak self] result in + switch result { + case .failure(let error): + self?.delegate?.show(error: error) + case .success(let id): + self?.fetchConnectionData(remoteURLString: nodeURL, id: id) + } + } + } + + private func detectConnectionAndHandle( + considerStatus: Bool = true, + reconnect: Bool, + subscription: SentinelWallet.Subscription + ) { + detectConnection { [weak self] result in + guard let self = self else { return } + + switch result { + case .failure(let error): + log.error(error) + if reconnect { + self.connect(to: subscription) + } + + case let .success((isTunnelActive, isSessionActive)): + switch (isTunnelActive, isSessionActive) { + case (true, true): + self.update(subscriptionInfo: subscription) + case (false, true): + if let tunnel = self.context.tunnelManager.lastTunnel { + self.context.tunnelManager.startActivation(of: tunnel) + self.update(subscriptionInfo: subscription) + } else { + if reconnect { + self.connect(to: subscription) + } else { + self.delegate?.set(isLoading: false) + } + } + case (true, false), (false, false): + self.connect(to: subscription) + self.updateLocation(address: subscription.node) + } + } + } + } + + /// Checks if tunnel and session are active + private func detectConnection( + considerStatus: Bool = true, + completion: @escaping (Result<(Bool, Bool), Error>) -> Void + ) { + var isTunnelActive: Bool + + if let tunnel = context.tunnelManager.lastTunnel { + isTunnelActive = true + + if considerStatus { + isTunnelActive = tunnel.status == .connected || tunnel.status == .connecting + } + } else { + isTunnelActive = false + } + + guard let sessionId = context.connectionInfoStorage.lastSessionId(), + let node = context.connectionInfoStorage.lastSelectedNode() else { + completion(.success((isTunnelActive, false))) + return + } + + context.sessionsService.loadActiveSessions { result in + switch result { + case .failure(let error): + completion(.failure(error)) + + case .success(let session): + guard session.contains(where: { $0.id == sessionId && $0.node == node }) else { + completion(.success((isTunnelActive, false))) + return + } + + completion(.success((isTunnelActive, true))) + } + } + } +} + +// MARK: - Network and Wallet work + +extension ConnectionNodeModel { + private func fetchConnectionData(remoteURLString: String, id: UInt64) { + var int = id.bigEndian + let sessionIdData = Data(bytes: &int, count: 8) + + guard let signature = context.walletService.generateSignature(for: sessionIdData) else { + delegate?.show(error: ConnectionModelError.signatureGenerationFailed) + return + } + + guard let selectedNode = self.selectedNode else { + delegate?.show(error: ConnectionModelError.noSelectedNode) + return + } + + context.sessionsService.fetchConnectionData( + remoteURLString: remoteURLString, + id: id, + accountAddress: context.walletStorage.walletAddress, + signature: signature + ) { [weak self] result in + guard let self = self else { return } + switch result { + case let .failure(error): + switch error { + case .noQuota: + self.delegate?.openPlans(node: selectedNode, resubscribe: true) + self.delegate?.set(isLoading: false) + case .nodeMisconfigured: + self.delegate?.suggestUnsubscribe(from: selectedNode) + self.delegate?.set(isLoading: false) + default: + self.delegate?.show(error: error) + } + case let .success((data, wgKey)): + self.context.connectionInfoStorage.set(sessionId: Int(id)) + self.context.tunnelManager.createNewProfile( + from: data, + with: wgKey + ) + } + } + } +} diff --git a/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Root/RouteCollections/Tunnel/Connection/Models/ConnectionModelError.swift b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Root/RouteCollections/Tunnel/Connection/Models/ConnectionModelError.swift new file mode 100644 index 0000000..32f7a88 --- /dev/null +++ b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Root/RouteCollections/Tunnel/Connection/Models/ConnectionModelError.swift @@ -0,0 +1,17 @@ +// +// ConnectionModelError.swift +// SOLARdVPNCommunityCoreiOS +// +// Created by Lika Vorobeva on 30.09.2022. +// + +import Foundation + +enum ConnectionModelError: Error { + case signatureGenerationFailed + case nodeIsOffline + case balanceUpdateFailed + case noSelectedNode + case noSubscription + case noQuotaLeft +} diff --git a/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Root/RouteCollections/Tunnel/Connection/Models/ConnectionModelType.swift b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Root/RouteCollections/Tunnel/Connection/Models/ConnectionModelType.swift new file mode 100644 index 0000000..dafa110 --- /dev/null +++ b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Root/RouteCollections/Tunnel/Connection/Models/ConnectionModelType.swift @@ -0,0 +1,25 @@ +// +// ConnectionModelType.swift +// SOLARdVPNCommunityCoreiOS +// +// Created by Lika Vorobeva on 30.09.2022. +// + +import Foundation + +protocol ConnectionModelDelegate: AnyObject { + func show(error: Error) + func show(warning: Error) + + func set(isLoading: Bool) +} + +protocol ConnectionModelType: AnyObject { + func connect() + func refreshNode() +} + +protocol NodeModelDelegate: AnyObject { + func openPlans(node: Node, resubscribe: Bool) + func suggestUnsubscribe(from node: Node) +} diff --git a/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Root/RouteCollections/Tunnel/PostConnectionRequest.swift b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Root/RouteCollections/Tunnel/PostConnectionRequest.swift new file mode 100644 index 0000000..01bbe0d --- /dev/null +++ b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Root/RouteCollections/Tunnel/PostConnectionRequest.swift @@ -0,0 +1,24 @@ +// +// PostConnectionRequest.swift +// SOLARdVPNCommunityCoreiOS +// +// Created by Lika Vorobeva on 30.09.2022. +// + +import Foundation + +struct PostConnectionRequest: Codable { + let nodeAddress: String + + init(nodeAddress: String) { + self.nodeAddress = nodeAddress + } +} + +// MARK: - Codable implementation + +extension PostConnectionRequest { + enum CodingKeys: String, CodingKey { + case nodeAddress + } +} diff --git a/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Root/RouteCollections/Tunnel/TunnelRouteCollection.swift b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Root/RouteCollections/Tunnel/TunnelRouteCollection.swift new file mode 100644 index 0000000..8ff410b --- /dev/null +++ b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Root/RouteCollections/Tunnel/TunnelRouteCollection.swift @@ -0,0 +1,102 @@ +// +// TunnelRouteCollection.swift +// SOLARdVPNCommunityCoreiOS +// +// Created by Lika Vorobeva on 30.09.2022. +// + +import Vapor +import Combine + +enum TunnelRouteEvent: String { + case alreadyConnected + case subscriptionCanceled +} + +class TunnelRouteCollection: RouteCollection { + private let model: ConnectionModel + private var cancellables = Set() + + // Connection Status + @Published private(set) var isConnected: Bool = false + private weak var delegate: WebSocketDelegate? + + init(model: ConnectionModel, delegate: WebSocketDelegate?) { + self.model = model + self.delegate = delegate + + subscribeToEvents() + model.setInitData() + } + + func boot(routes: RoutesBuilder) throws { + routes.post("connection", use: createNewSession) + routes.delete("connection", use: startDeactivationOfActiveTunnel) + } +} + +extension TunnelRouteCollection { +#warning("TODO add result") + func startDeactivationOfActiveTunnel(_ req: Request) async throws -> Bool { + model.disconnect() + + return true + } + +#warning("TODO add result & pass node") + func createNewSession(_ req: Request) async throws -> Bool { + if isConnected { + return true // && add status TunnelRouteEvent + } + model.connect() + return true + } +} + +// MARK: - Subscribe to events + +extension TunnelRouteCollection { + private func subscribeToEvents() { + model.eventPublisher + .receive(on: DispatchQueue.main) + .sink { [weak self] event in + guard let self = self else { return } + switch event { + case let .error(error): + self.send(error: error) + case let .warning(warning): + self.send(warning: warning) + case let .info(text): + self.send(info: text) + case let .updateTunnelActivity(isActive): + self.updateConnection(isConnected: isActive) + } + } + .store(in: &cancellables) + } +} + +// MARK: - Handle events + +extension TunnelRouteCollection { + private func send(error: Error) { +#warning("TODO: send json with error") + delegate?.send(event: error.localizedDescription) + } + + private func send(warning: Error) { +#warning("TODO: send json with warning") + delegate?.send(event: warning.localizedDescription) + } + + private func send(info: String) { +#warning("TODO: send json with info") + delegate?.send(event: info) + } + + private func updateConnection(isConnected: Bool) { +#warning("TODO: send json with isConnected") + self.isConnected = isConnected + delegate?.send(event: "isConnected \(isConnected)") + } +} diff --git a/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Root/WebSocketDelegate.swift b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Root/WebSocketDelegate.swift new file mode 100644 index 0000000..1718f02 --- /dev/null +++ b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Root/WebSocketDelegate.swift @@ -0,0 +1,12 @@ +// +// WebSocketDelegate.swift +// SOLARdVPNCommunityCoreiOS +// +// Created by Lika Vorobeva on 04.10.2022. +// + +import Foundation + +protocol WebSocketDelegate: AnyObject { + func send(event: String) +} diff --git a/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Services/DNSServers/DNSServerType.swift b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Services/DNSServers/DNSServerType.swift index 387d213..a375486 100644 --- a/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Services/DNSServers/DNSServerType.swift +++ b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Services/DNSServers/DNSServerType.swift @@ -24,6 +24,6 @@ enum DNSServerType: String, CaseIterable { } static var `default`: DNSServerType { - return .handshake + .handshake } } diff --git a/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Services/NodesService/Node.swift b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Services/NodesService/Node.swift new file mode 100644 index 0000000..8846895 --- /dev/null +++ b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Services/NodesService/Node.swift @@ -0,0 +1,17 @@ +// +// Node.swift +// SOLARdVPNCommunityCoreiOS +// +// Created by Lika Vorobeva on 03.10.2022. +// + +import Foundation +import SOLARAPI + +typealias Node = SOLARAPI.Node + +extension Node { + var truncatedAddress: String { + "sent..." + String(address).suffix(8) + } +} diff --git a/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Services/SecurityService/SecurityService.swift b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Services/SecurityService/SecurityService.swift new file mode 100644 index 0000000..7f8dc97 --- /dev/null +++ b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Services/SecurityService/SecurityService.swift @@ -0,0 +1,73 @@ +// +// SecurityService.swift +// SOLARdVPNCommunityCoreiOS +// +// Created by Lika Vorobeva on 05.10.2022. +// + +import Foundation +import SentinelWallet +import HDWallet +import SwiftKeychainWrapper + +enum SecurityServiceError: Error { + case emptyInput + case invalidInput +} + +private struct Constants { + let key = "password" + let mnemonicsCount = 24 +} +private let constants = Constants() + +final public class SecurityService: SecurityServiceType { + private let keychain: KeychainWrapper + + public init( + keychain: KeychainWrapper = .init( + serviceName: "SecurityService", + accessGroup: "group.ee.solarlabs.community-core.ios" + ) + ) { + self.keychain = keychain + } + + public func save(mnemonics: [String], for account: String) -> Bool { + let mnemonicString = mnemonics.joined(separator: " ") + return keychain.set( + mnemonicString, + forKey: account.sha1(), + withAccessibility: .afterFirstUnlockThisDeviceOnly + ) + } + + public func loadMnemonics(for account: String) -> [String]? { + keychain + .string(forKey: account.sha1())? + .trimmingCharacters(in: .whitespacesAndNewlines) + .components(separatedBy: " ") + } + + public func mnemonicsExists(for account: String) -> Bool { + keychain.hasValue(forKey: account.sha1()) + } + + public func restore(from mnemonics: [String]) -> Result { + guard !mnemonics.isEmpty else { + return .failure(SecurityServiceError.emptyInput) + } + + guard + mnemonics.count == constants.mnemonicsCount, mnemonics.allSatisfy({ WordList.english.words.contains($0) }) + else { + return .failure(SecurityServiceError.invalidInput) + } + + guard let restoredAddress = restoreAddress(for: mnemonics) else { + return .failure(SecurityServiceError.invalidInput) + } + + return .success(restoredAddress) + } +} diff --git a/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Services/SessionsService/SessionServiceError.swift b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Services/SessionsService/SessionServiceError.swift new file mode 100644 index 0000000..a4c496a --- /dev/null +++ b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Services/SessionsService/SessionServiceError.swift @@ -0,0 +1,38 @@ +// +// SessionsServiceError.swift +// SOLARdVPNCommunityCoreiOS +// +// Created by Lika Vorobeva on 29.11.2021. +// + +import Foundation + +enum SessionsServiceError: LocalizedError { + case invalidURL + + case connectionParsingFailed + case nodeMisconfigured + case noQuota + + case serverLocalized(String) + case other(Error) +} + +extension SessionsServiceError { + static var allCases: [SessionsServiceError] { + [.connectionParsingFailed, .nodeMisconfigured, .noQuota] + } + + var innerCodes: [Int] { + switch self { + case .nodeMisconfigured: + return [3, 4, 5] + case .noQuota: + return [9, 10] + case .connectionParsingFailed: + return [6, 7, 8] + default: + return [] + } + } +} diff --git a/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Services/SessionsService/SessionsService.swift b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Services/SessionsService/SessionsService.swift new file mode 100644 index 0000000..1d1689d --- /dev/null +++ b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Services/SessionsService/SessionsService.swift @@ -0,0 +1,100 @@ +// +// SessionsService.swift +// SOLARdVPNCommunityCoreiOS +// +// Created by Lika Vorobeva on 20.07.2022. +// + +import Foundation +import SentinelWallet +import SOLARAPI +import WireGuardKit +import Vapor + +final class SessionsService { + private let walletService: WalletService + private let sessionProvider: NodeSessionProviderType + private let subscriptionsProvider: SubscriptionsProviderType + + init( + sessionProvider: NodeSessionProviderType, + subscriptionsProvider: SubscriptionsProviderType, + walletService: WalletService + ) { + self.sessionProvider = sessionProvider + self.subscriptionsProvider = subscriptionsProvider + self.walletService = walletService + } +} + +// MARK: - SubscriptionsServiceType + +extension SessionsService: SessionsServiceType { + func loadActiveSessions(completion: @escaping (Result<[SentinelWallet.Session], Error>) -> Void) { + subscriptionsProvider.queryActiveSessions(for: walletService.currentWalletAddress, completion: completion) + } + + func stopActiveSessions(completion: @escaping (Result) -> Void) { + guard let sender = walletService.createTransactionSender() else { + completion(.failure(SubscriptionsServiceError.missingMnemonic)) + return + } + + subscriptionsProvider.stopActiveSessions(sender: sender, completion: completion) + } + + func startSession(on subscriptionID: UInt64, node: String, completion: @escaping (Result) -> Void) { + guard let sender = walletService.createTransactionSender() else { + completion(.failure(SubscriptionsServiceError.missingMnemonic)) + return + } + + subscriptionsProvider.startNewSession(on: subscriptionID, sender: sender, node: node, completion: completion) + } + + func fetchConnectionData( + remoteURLString: String, + id: UInt64, + accountAddress: String, + signature: String, + completion: @escaping (Result<(Data, PrivateKey), SessionsServiceError>) -> Void + ) { + guard var components = URLComponents(string: remoteURLString) else { + completion(.failure(SessionsServiceError.invalidURL)) + return + } + components.scheme = "http" + + guard let urlString = components.string, let remoteURL = URL(string: urlString) else { + completion(.failure(SessionsServiceError.invalidURL)) + return + } + let wgKey = PrivateKey() + + sessionProvider.createClient( + remoteURL: remoteURL, + address: accountAddress, + id: "\(id)", + request: .init(key: wgKey.publicKey.base64Key, signature: signature)) { result in + switch result { + case .success(let infoResult): + guard infoResult.success, let stringData = infoResult.result else { + completion(.failure(.connectionParsingFailed)) + return + } + guard let data = Data(base64Encoded: stringData), data.bytes.count == 58 else { + completion(.failure(.connectionParsingFailed)) + return + } + + completion(.success((data, wgKey))) + case .failure(let error): + #warning("TODO: map error") +// let mapper = Self.mapNetworkError(handleSpecificGenericErrors: { error -> SessionsServiceError? in +// return SessionsServiceError.allCases.first(where: { $0.innerCodes.contains(error.code) }) +// }) +// completion(.failure(error)) + } + } + } +} diff --git a/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Services/SessionsService/SessionsServiceType.swift b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Services/SessionsService/SessionsServiceType.swift new file mode 100644 index 0000000..d937d31 --- /dev/null +++ b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Services/SessionsService/SessionsServiceType.swift @@ -0,0 +1,23 @@ +// +// SessionsServiceType.swift +// SOLARdVPNCommunityCoreiOS +// +// Created by Lika Vorobeva on 20.07.2022. +// + +import Foundation +import SentinelWallet +import WireGuardKit + +protocol SessionsServiceType { + func loadActiveSessions(completion: @escaping (Result<[Session], Error>) -> Void) + func stopActiveSessions(completion: @escaping (Result) -> Void) + func startSession(on subscriptionID: UInt64, node: String, completion: @escaping (Result) -> Void) + func fetchConnectionData( + remoteURLString: String, + id: UInt64, + accountAddress: String, + signature: String, + completion: @escaping (Result<(Data, PrivateKey), SessionsServiceError>) -> Void + ) +} diff --git a/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Services/Storage/GeneralSettingsStorage/GeneralSettingsStorage.swift b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Services/Storage/GeneralSettingsStorage/GeneralSettingsStorage.swift new file mode 100644 index 0000000..d96e21b --- /dev/null +++ b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Services/Storage/GeneralSettingsStorage/GeneralSettingsStorage.swift @@ -0,0 +1,88 @@ +// +// GeneralSettingsStorage.swift +// SOLARdVPNCommunityCoreiOS +// +// Created by Lika Vorobeva on 28.09.2022. +// + +import Foundation +import Accessibility + +private enum Keys: String, CaseIterable { + case walletKey + case dnsKey + + case lastSelectedNodeKey + case lastSessionKey + case sessionStart +} + +final class GeneralSettingsStorage { + private let settingsStorageStrategy: SettingsStorageStrategyType + + @Published private(set) var _preselectedNode: String? + + init(settingsStorageStrategy: SettingsStorageStrategyType = UserDefaultsStorageStrategy()) { + self.settingsStorageStrategy = settingsStorageStrategy + } +} + +// MARK: - StoresWallet + +extension GeneralSettingsStorage: StoresWallet { + func set(wallet: String) { + settingsStorageStrategy.setObject(wallet, forKey: Keys.walletKey.rawValue) + } + + var walletAddress: String { + settingsStorageStrategy.object(ofType: String.self, forKey: Keys.walletKey.rawValue) ?? "" + } +} + +// MARK: - StoresDNSServers + +extension GeneralSettingsStorage: StoresDNSServers { + func set(dns: DNSServerType) { + settingsStorageStrategy.setObject(dns.rawValue, forKey: Keys.dnsKey.rawValue) + } + + var selectedDNS: DNSServerType { + guard let rawValue = settingsStorageStrategy.object(ofType: String.self, forKey: Keys.dnsKey.rawValue) else { + return .default + } + + guard let server = DNSServerType(rawValue: rawValue) else { + return .default + } + + return server + } +} + +// MARK: - StoresConnectInfo + +extension GeneralSettingsStorage: StoresConnectInfo { + func set(lastSelectedNode: String?) { + settingsStorageStrategy.setObject(lastSelectedNode, forKey: Keys.lastSelectedNodeKey.rawValue) + } + + func lastSelectedNode() -> String? { + settingsStorageStrategy.object(ofType: String.self, forKey: Keys.lastSelectedNodeKey.rawValue) + } + + func set(sessionId: Int?) { + settingsStorageStrategy.setObject(sessionId, forKey: Keys.lastSessionKey.rawValue) + } + + func lastSessionId() -> Int? { + settingsStorageStrategy.object(ofType: Int.self, forKey: Keys.lastSessionKey.rawValue) + } + + func set(sessionStart: Date?) { + settingsStorageStrategy.setObject(sessionStart, forKey: Keys.sessionStart.rawValue) + } + + func lastSessionStart() -> Date? { + settingsStorageStrategy.object(ofType: Date.self, forKey: Keys.sessionStart.rawValue) + } +} diff --git a/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Services/Storage/StorageStrategy/KeychainStorageStrategy.swift b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Services/Storage/StorageStrategy/KeychainStorageStrategy.swift new file mode 100644 index 0000000..ee6fc1f --- /dev/null +++ b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Services/Storage/StorageStrategy/KeychainStorageStrategy.swift @@ -0,0 +1,54 @@ +// +// KeychainStorageStrategy.swift +// SOLARdVPNCommunityCoreiOS +// +// Created by Lika Vorobeva on 28.09.2022. +// + +import Foundation +import KeychainAccess + +// MARK: - KeychainStorageStrategy + +final class KeychainStorageStrategy { + private let keychain: KeychainAccess.Keychain + + init(serviceKey: String) { + self.keychain = KeychainAccess.Keychain(service: serviceKey) + } +} + +extension KeychainStorageStrategy: SettingsStorageStrategyType { + func object(ofType type: T.Type, forKey key: String) -> T? { + if let data = try? keychain.getData(key), + let object = Serializer.fromData(data, withType: type.self) { + return object + } + return nil + } + + func setObject(_ object: T, forKey key: String) -> Bool { + if let encoded = object.toData() { + do { + try keychain.set(encoded, key: key) + return true + } catch { + return false + } + } + return false + } + + func existsObject(forKey key: String) -> Bool { + return (try? keychain.getData(key)) != nil + } + + func removeObject(forKey key: String) -> Bool { + do { + try keychain.remove(key) + return true + } catch { + return false + } + } +} diff --git a/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Services/Storage/StorageStrategy/SettingsStorageStrategyType.swift b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Services/Storage/StorageStrategy/SettingsStorageStrategyType.swift new file mode 100644 index 0000000..1fd0a8c --- /dev/null +++ b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Services/Storage/StorageStrategy/SettingsStorageStrategyType.swift @@ -0,0 +1,20 @@ +// +// SettingsStorageStrategyType.swift +// SOLARdVPNCommunityCoreiOS +// +// Created by Lika Vorobeva on 28.09.2022. +// + +import Foundation + +protocol SettingsStorageStrategyType: AnyObject { + func object(ofType type: T.Type, forKey key: String) -> T? + + @discardableResult + func setObject(_ object: T, forKey key: String) -> Bool + + func existsObject(forKey key: String) -> Bool + + @discardableResult + func removeObject(forKey key: String) -> Bool +} diff --git a/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Services/Storage/StorageStrategy/UserDefaultsStorageStrategy.swift b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Services/Storage/StorageStrategy/UserDefaultsStorageStrategy.swift new file mode 100644 index 0000000..2f54128 --- /dev/null +++ b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Services/Storage/StorageStrategy/UserDefaultsStorageStrategy.swift @@ -0,0 +1,41 @@ +// +// StorageService.swift +// SOLARdVPNCommunityCoreiOS +// +// Created by Lika Vorobeva on 28.09.2022. +// + +import Foundation + +final class UserDefaultsStorageStrategy { + private let defaults = UserDefaults.standard +} + +// MARK: SettingsStorageStrategyType implementation + +extension UserDefaultsStorageStrategy: SettingsStorageStrategyType { + func object(ofType type: T.Type, forKey key: String) -> T? { + if let data = defaults.value(forKey: key) as? Data, + let object = Serializer.fromData(data, withType: type.self) { + return object + } + return nil + } + + func setObject(_ object: T, forKey key: String) -> Bool { + if let encoded = Serializer.toData(from: object) { + defaults.set(encoded, forKey: key) + return true + } + return false + } + + func existsObject(forKey key: String) -> Bool { + return defaults.object(forKey: key) != nil + } + + func removeObject(forKey key: String) -> Bool { + defaults.set(nil, forKey: key) + return true + } +} diff --git a/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Services/Storage/StoresConnectInfo.swift b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Services/Storage/StoresConnectInfo.swift new file mode 100644 index 0000000..404ba97 --- /dev/null +++ b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Services/Storage/StoresConnectInfo.swift @@ -0,0 +1,16 @@ +// +// StoresConnectInfo.swift +// SOLARdVPNCommunityCoreiOS +// +// Created by Lika Vorobeva on 03.10.2022. +// + +import Foundation + +protocol StoresConnectInfo { + func set(lastSelectedNode: String?) + func lastSelectedNode() -> String? + + func set(sessionId: Int?) + func lastSessionId() -> Int? +} diff --git a/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Services/DNSServers/StoresDNSServers.swift b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Services/Storage/StoresDNSServers.swift similarity index 100% rename from SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Services/DNSServers/StoresDNSServers.swift rename to SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Services/Storage/StoresDNSServers.swift diff --git a/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Services/Storage/StoresWallet.swift b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Services/Storage/StoresWallet.swift new file mode 100644 index 0000000..c3746ae --- /dev/null +++ b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Services/Storage/StoresWallet.swift @@ -0,0 +1,13 @@ +// +// StoresWallet.swift +// SOLARdVPNCommunityCoreiOS +// +// Created by Lika Vorobeva on 28.09.2022. +// + +import Foundation + +protocol StoresWallet { + func set(wallet: String) + var walletAddress: String { get } +} diff --git a/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Services/SubscriptionsService/SubscriptionsService.swift b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Services/SubscriptionsService/SubscriptionsService.swift new file mode 100644 index 0000000..593e45d --- /dev/null +++ b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Services/SubscriptionsService/SubscriptionsService.swift @@ -0,0 +1,138 @@ +// +// SubscriptionsService.swift +// SOLARdVPNCommunityCoreiOS +// +// Created by Lika Vorobeva on 20.07.2022. +// + +import Foundation +import Combine +import SentinelWallet + +enum SubscriptionsServiceError: LocalizedError { + case missingMnemonic + case paymentFailed + case faliToCancelSubscription + case activeSession +} + +final class SubscriptionsService { + private let walletService: WalletService + private let subscriptionsProvider: SubscriptionsProviderType + + init( + subscriptionsProvider: SubscriptionsProviderType, + walletService: WalletService + ) { + self.subscriptionsProvider = subscriptionsProvider + self.walletService = walletService + } +} + +// MARK: - SubscriptionsServiceType + +extension SubscriptionsService: SubscriptionsServiceType { + func loadActiveSubscriptions(completion: @escaping (Result<[SentinelWallet.Subscription], Error>) -> Void) { + subscriptionsProvider.querySubscriptions( + for: walletService.currentWalletAddress, + with: .active + ) { result in + switch result { + case .failure(let error): + completion(.failure(error)) + case .success(let subscriptions): + completion(.success(subscriptions)) + } + } + } + + func checkBalanceAndSubscribe( + to node: String, + deposit: CoinToken, + completion: @escaping (Result) -> Void + ) { + walletService.fetchBalance { [weak self] result in + guard let self = self else { return } + switch result { + case .failure(let error): + completion(.failure(error)) + case .success(let balances): + let balance = balances + .first(where: { $0.denom == deposit.denom }) + + guard let balance = balance, + Int(balance.amount) ?? 0 >= (Int(deposit.amount) ?? 0 + self.walletService.fee) else { + completion(.success(false)) + return + } + + self.subscribe(to: node, with: deposit, completion: completion) + } + } + } + + func cancel(subscriptions: [UInt64], with nodeAddress: String, completion: @escaping (Result) -> Void) { + guard let sender = walletService.createTransactionSender() else { + completion(.failure(SubscriptionsServiceError.missingMnemonic)) + return + } + + subscriptionsProvider.cancel(subscriptions: subscriptions, sender: sender, node: nodeAddress) { result in + switch result { + case let .failure(error): + completion(.failure(error)) + case let .success(result): + switch result.isSuccess { + case true: + completion(.success(())) + case false: + // Subscription can not be cancelled if active sessions exist + if result.rawLog.contains("can not cancel") { + completion(.failure(SubscriptionsServiceError.activeSession)) + return + } + completion(.failure(SubscriptionsServiceError.faliToCancelSubscription)) + } + } + } + } + + func queryQuota(for subscription: UInt64, completion: @escaping (Result) -> Void) { + subscriptionsProvider.queryQuota( + address: walletService.currentWalletAddress, + subscriptionId: subscription + ) { result in + switch result { + case let .failure(error): + completion(.failure(error)) + case let .success(quota): + completion(.success(quota)) + } + } + } +} + +// MARK: - Private methods + +extension SubscriptionsService { + private func subscribe(to node: String, with deposit: CoinToken, completion: @escaping (Result) -> Void) { + guard let sender = walletService.createTransactionSender() else { + completion(.failure(SubscriptionsServiceError.missingMnemonic)) + return + } + + subscriptionsProvider.subscribe(sender: sender, node: node, deposit: deposit) { result in + switch result { + case .failure(let error): + completion(.failure(error)) + case .success(let response): + guard response.isSuccess else { + completion(.failure(SubscriptionsServiceError.paymentFailed)) + return + } + + completion(.success(true)) + } + } + } +} diff --git a/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Services/SubscriptionsService/SubscriptionsServiceType.swift b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Services/SubscriptionsService/SubscriptionsServiceType.swift new file mode 100644 index 0000000..51455e8 --- /dev/null +++ b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Services/SubscriptionsService/SubscriptionsServiceType.swift @@ -0,0 +1,20 @@ +// +// SubscriptionsServiceType.swift +// SOLARdVPNCommunityCoreiOS +// +// Created by Lika Vorobeva on 20.07.2022. +// + +import Foundation +import SentinelWallet + +protocol SubscriptionsServiceType { + func loadActiveSubscriptions(completion: @escaping (Result<[SentinelWallet.Subscription], Error>) -> Void) + func checkBalanceAndSubscribe( + to node: String, + deposit: CoinToken, + completion: @escaping (Result) -> Void + ) + func cancel(subscriptions: [UInt64], with nodeAddress: String, completion: @escaping (Result) -> Void) + func queryQuota(for subscription: UInt64, completion: @escaping (Result) -> Void) +}