Commit e7baece0 authored by Jose Blaya's avatar Jose Blaya

Merge branch 'feature/dedicated_ip' into develop

parents ba3cd1f5 7aa5190e
Pod::Spec.new do |s|
s.name = "PIALibrary"
s.version = "2.8.1"
s.version = "2.8.2"
s.summary = "PIA client library in Swift."
s.homepage = "https://www.privateinternetaccess.com/"
......
This diff is collapsed.
......@@ -41,9 +41,9 @@
<CodeCoverageTargets>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "0EBFFFD41F693F800009D4F4"
BuildableName = "PIALibrary.framework"
BlueprintName = "PIALibrary-iOS"
BlueprintIdentifier = "0EE78B0B1F818A32002E4CDD"
BuildableName = "PIALibraryHost-iOS.app"
BlueprintName = "PIALibraryHost-iOS"
ReferencedContainer = "container:PIALibrary.xcodeproj">
</BuildableReference>
</CodeCoverageTargets>
......
......@@ -137,6 +137,14 @@ public protocol AccountProvider: class {
*/
func cleanDatabase()
/**
Returns the enabled features in the app.
- Parameter callback: Returns `nil` on success.
*/
func featureFlags(_ callback: SuccessLibraryCallback?)
#if os(iOS)
/**
Lists the available plans with their corresponding product to purchase in order to get them.
......
......@@ -41,9 +41,7 @@ protocol PlainStore: class {
var cachedServers: [Server] { get set }
var preferredServer: Server? { get set }
var serverNetwork: ServersNetwork { get set }
func ping(forServerIdentifier serverIdentifier: String) -> Int?
func setPing(_ ping: Int, forServerIdentifier serverIdentifier: String)
......
......@@ -46,5 +46,13 @@ protocol SecureStore: class {
func tokenKey(for username: String) -> String
func dipTokens() -> [String]?
func setDIPToken(_ dipToken: String)
func remove(_ dipToken: String)
func removeDIPTokens()
func clear(for username: String)
}
//
// ServerNetwork.swift
// PIALibrary
//
// Created by Jose Antonio Blaya Garcia on 06/05/2020.
// Copyright © 2020 Private Internet Access, Inc.
//
// This file is part of the Private Internet Access iOS Client.
//
// The Private Internet Access iOS Client is free software: you can redistribute it and/or
// modify it under the terms of the GNU General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option) any later version.
//
// The Private Internet Access iOS Client is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
// details.
//
// You should have received a copy of the GNU General Public License along with the Private
// Internet Access iOS Client. If not, see <https://www.gnu.org/licenses/>.
//
import Foundation
public enum ServersNetwork: String {
case legacy
case gen4
}
......@@ -46,6 +46,9 @@ public protocol ServerProvider: class {
var regionStaticData: RegionData! { get }
/// The array of DIP tokens stored in the keychain, or `nil` if logged out.
var dipTokens: [String]? { get }
/**
Loads this provider with a local JSON, as seen on the /servers web client API.
......@@ -94,4 +97,31 @@ public protocol ServerProvider: class {
Reset the currentServers object
*/
func resetCurrentServers()
/**
Activates the dedicated IP tokens.
- Precondition: `isLoggedIn` is `true`.
- Parameter tokens: The `String` array of DIP token to activate.
- Parameter callback: Returns the status of the DIP region `Server` array.
*/
func activateDIPTokens(_ tokens: [String], _ callback: LibraryCallback<[Server]>?)
/**
Activates the dedicated IP token.
- Precondition: `isLoggedIn` is `true`.
- Parameter tokens: The `String` DIP token to activate.
- Parameter callback: Returns the status of the DIP region `Server`.
*/
func activateDIPToken(_ token: String, _ callback: LibraryCallback<Server?>?)
/**
Removes the dedicated IP region.
- Precondition: `isLoggedIn` is `true`.
- Parameter dipToken: The `String` DIP token to remove.
*/
func removeDIPToken(_ dipToken: String)
}
......@@ -117,6 +117,14 @@ public protocol VPNProvider: class {
- Parameter callback: Returns the `Usage` information on success.
*/
func dataUsage(_ callback: LibraryCallback<Usage>?)
/**
Check if the VPN profile needs to be migrated to GEN4.
- Precondition: isVPNConnected == true
- Returns: `Bool`
*/
func needsMigrationToGEN4() -> Bool
}
public extension VPNProvider {
......
//
// DedicatedIP.swift
// PIALibrary
//
// Created by Jose Blaya on 13/10/2020.
// Copyright © 2020 Private Internet Access, Inc.
//
// This file is part of the Private Internet Access iOS Client.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software
// without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
// CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import Foundation
public enum DedicatedIPStatus {
case active
case expired
case invalid
}
......@@ -134,9 +134,7 @@ public class Server: Hashable {
/// The best server IPs for establishing an IKEv2 connection over UDP.
public let iKEv2AddressesForUDP: [ServerAddressIP]?
public let serverNetwork: ServersNetwork?
/// The address on which to "ping" the server.
///
/// - Seealso: `Macros.ping(...)`
......@@ -145,6 +143,12 @@ public class Server: Hashable {
/// The meta IP.
public let meta: ServerAddressIP?
public let dipExpire: Date?
public let dipToken: String?
public let dipStatus: DedicatedIPStatus?
var isAutomatic: Bool
/// :nodoc:
......@@ -161,9 +165,11 @@ public class Server: Hashable {
iKEv2AddressesForUDP: [ServerAddressIP]? = nil,
pingAddress: Address?,
responseTime: Int? = 0,
serverNetwork: ServersNetwork? = .legacy,
geo: Bool = false,
meta: ServerAddressIP? = nil,
dipExpire: Date? = nil,
dipToken: String? = nil,
dipStatus: DedicatedIPStatus? = nil,
regionIdentifier: String) {
self.serial = serial
......@@ -183,7 +189,10 @@ public class Server: Hashable {
self.meta = meta
self.pingAddress = pingAddress
self.serverNetwork = serverNetwork
self.dipExpire = dipExpire
self.dipToken = dipToken
self.dipStatus = dipStatus
isAutomatic = true
}
......@@ -192,7 +201,7 @@ public class Server: Hashable {
/// :nodoc:
public static func ==(lhs: Server, rhs: Server) -> Bool {
return (lhs.identifier == rhs.identifier)
return (lhs.identifier == rhs.identifier && lhs.dipToken == rhs.dipToken)
}
/// :nodoc:
......@@ -205,8 +214,7 @@ public class Server: Hashable {
extension Server {
func bestAddressForOpenVPNTCP() -> Address? {
if Client.configuration.serverNetwork == .gen4,
let addresses = openVPNAddressesForTCP {
if let addresses = openVPNAddressesForTCP {
let sorted = addresses.sorted(by: { $0.responseTime ?? 0 > $1.responseTime ?? 0 })
return nil
}
......@@ -216,8 +224,7 @@ extension Server {
}
func bestAddressForOpenVPNUDP() -> Address? {
if Client.configuration.serverNetwork == .gen4,
let addresses = openVPNAddressesForUDP {
if let addresses = openVPNAddressesForUDP {
let sorted = addresses.sorted(by: { $0.responseTime ?? 0 > $1.responseTime ?? 0 })
return nil
}
......@@ -228,8 +235,7 @@ extension Server {
func bestAddressForIKEv2() -> ServerAddressIP? {
if Client.configuration.serverNetwork == .gen4,
let addresses = iKEv2AddressesForUDP {
if let addresses = iKEv2AddressesForUDP {
let sorted = addresses.sorted(by: { $0.responseTime ?? 0 > $1.responseTime ?? 0 })
return sorted.first
}
......@@ -238,8 +244,7 @@ extension Server {
}
func bestAddressForWireGuard() -> ServerAddressIP? {
if Client.configuration.serverNetwork == .gen4,
let addresses = wireGuardAddressesForUDP {
if let addresses = wireGuardAddressesForUDP {
let sorted = addresses.sorted(by: { $0.responseTime ?? 0 > $1.responseTime ?? 0 })
return sorted.first
}
......@@ -247,37 +252,30 @@ extension Server {
return nil
}
func bestPingAddress() -> [Address] {
public func bestPingAddress() -> [Address] {
if Client.configuration.serverNetwork == .gen4 {
switch Client.providers.vpnProvider.currentVPNType {
case IKEv2Profile.vpnType:
var addresses: [Address] = []
for address in iKEv2AddressesForUDP ?? [] {
addresses.append(Address(hostname: address.ip, port: 0))
}
return addresses
case PIATunnelProfile.vpnType:
var addresses: [Address] = []
for address in openVPNAddressesForUDP ?? [] {
addresses.append(Address(hostname: address.ip, port: 0))
}
return addresses
case PIAWGTunnelProfile.vpnType:
var addresses: [Address] = []
for address in wireGuardAddressesForUDP ?? [] {
addresses.append(Address(hostname: address.ip, port: 0))
}
return addresses
default:
return []
switch Client.providers.vpnProvider.currentVPNType {
case IKEv2Profile.vpnType:
var addresses: [Address] = []
for address in iKEv2AddressesForUDP ?? [] {
addresses.append(Address(hostname: address.ip, port: 0))
}
return addresses
case PIATunnelProfile.vpnType:
var addresses: [Address] = []
for address in openVPNAddressesForUDP ?? [] {
addresses.append(Address(hostname: address.ip, port: 0))
}
} else if let pingAddress = pingAddress {
return [pingAddress]
} else {
return addresses
case PIAWGTunnelProfile.vpnType:
var addresses: [Address] = []
for address in wireGuardAddressesForUDP ?? [] {
addresses.append(Address(hostname: address.ip, port: 0))
}
return addresses
default:
return []
}
}
......
......@@ -40,6 +40,10 @@ protocol WebServices: class {
func loginLink(email: String, _ callback: SuccessLibraryCallback?)
// MARK: DIP Token
func activateDIPToken(tokens: [String], _ callback: LibraryCallback<[Server]>?)
/**
Invalidates the access token.
- Parameter callback: Returns an `Bool` if the token was expired.
......@@ -66,4 +70,6 @@ protocol WebServices: class {
func submitDebugLog(_ log: DebugLog, _ callback: SuccessLibraryCallback?)
func featureFlags(_ callback: LibraryCallback<[String]>?)
}
......@@ -319,6 +319,15 @@ class DefaultAccountProvider: AccountProvider, ConfigurationAccess, DatabaseAcce
}
}
func featureFlags(_ callback: SuccessLibraryCallback?) {
webServices.featureFlags { (features, nil) in
if let features = features, !features.isEmpty {
Client.configuration.featureFlags.append(contentsOf: features)
}
callback?(nil)
}
}
#if os(iOS)
func subscriptionInformation(_ callback: LibraryCallback<AppStoreInformation>?) {
log.debug("Fetching available product keys...")
......@@ -544,6 +553,7 @@ class DefaultAccountProvider: AccountProvider, ConfigurationAccess, DatabaseAcce
accessedDatabase.secure.clear(for: username)
accessedDatabase.secure.setToken(nil, for: accessedDatabase.secure.tokenKey(for: username))
}
accessedDatabase.secure.removeDIPTokens()
accessedDatabase.secure.setPublicUsername(nil)
accessedDatabase.plain.accountInfo = nil
accessedDatabase.plain.visibleTiles = AvailableTiles.defaultTiles()
......
......@@ -144,8 +144,8 @@ extension Client {
/// Store the account password in memory when the email is set and the user is LoggedIn.
public var tempAccountPassword: String
/// The server network to use.
private(set) var serverNetwork: ServersNetwork
/// Enabled features
public var featureFlags: [String]
// MARK: Initialization
......@@ -218,8 +218,8 @@ extension Client {
maxQuickConnectServers = 6
tempAccountPassword = ""
serverNetwork = Client.database.plain.serverNetwork
featureFlags = []
}
// MARK: WebServices
......@@ -307,15 +307,6 @@ extension Client {
#endif
public func setServerNetworks(to serverNetwork: ServersNetwork) {
self.serverNetwork = serverNetwork
Client.database.plain.serverNetwork = serverNetwork
}
public func currentServerNetwork() -> ServersNetwork {
return Client.database.plain.serverNetwork
}
// public init(name: String) {
// guard let path = Bundle.main.path(forResource: name, ofType: "plist") else {
// fatalError("Unable to read configuration from \(name).plist")
......
......@@ -54,50 +54,27 @@ class PingTask {
let persistence = Client.database.plain
self.state = .pending
if Client.configuration.serverNetwork == .gen4 {
log.debug("Starting to Ping \(server.identifier) with address: \(address.hostname)")
queue.async() { [weak self] in
guard let address = self?.address, let server = self?.server else {
return
}
let tcpAddress = Server.Address(hostname: address.hostname, port: 443)
response = server.ping(toAddress: tcpAddress, withProtocol: .TCP)
DispatchQueue.main.async {
self?.parsePingResponse(response: response, withServer: server)
if let responseTime = response {
server.updateResponseTime(responseTime, forAddress: address)
persistence.setPing(responseTime, forServerIdentifier: server.identifier)
}
self?.state = .completed
}
log.debug("Starting to Ping \(server.identifier) with address: \(address.hostname)")
queue.async() { [weak self] in
guard let address = self?.address, let server = self?.server else {
return
}
} else {
queue.async() { [weak self] in
guard let address = self?.address, let server = self?.server else {
return
}
response = server.ping(toAddress: address, withProtocol: .UDP)
DispatchQueue.main.async {
self?.parsePingResponse(response: response, withServer: server)
if let responseTime = response {
server.updateResponseTime(responseTime, forAddress: address)
persistence.setPing(responseTime, forServerIdentifier: server.identifier)
}
self?.state = .completed
let tcpAddress = Server.Address(hostname: address.hostname, port: 443)
response = server.ping(toAddress: tcpAddress, withProtocol: .TCP)
DispatchQueue.main.async {
self?.parsePingResponse(response: response, withServer: server)
if let responseTime = response {
server.updateResponseTime(responseTime, forAddress: address)
persistence.setPing(responseTime, forServerIdentifier: server.identifier)
}
self?.state = .completed
}
}
}
private func parsePingResponse(response: Int?, withServer server: Server) {
......
......@@ -171,15 +171,7 @@ class ServersDaemon: Daemon, ConfigurationAccess, DatabaseAccess, ProvidersAcces
@objc private func applicationDidBecomeActive(notification: Notification) {
if hasEnabledUpdates {
let currentServers = accessedProviders.serverProvider.currentServers
if let serverNetwork = currentServers.first?.serverNetwork, serverNetwork != Client.configuration.serverNetwork {
Client.providers.serverProvider.resetCurrentServers()
if let data = Client.configuration.bundledServersJSON {
Client.providers.serverProvider.loadLocalJSON(fromJSON: data)
}
pingIfOffline(servers: accessedProviders.serverProvider.currentServers)
} else {
pingIfOffline(servers: currentServers)
}
pingIfOffline(servers: currentServers)
}
}
......
......@@ -117,3 +117,23 @@ extension KeychainStore {
}
}
extension KeychainStore {
func dipTokens() -> [String]? {
return try? backend.dipTokens()
}
func setDIPToken(_ dipToken: String) {
try? backend.set(dipToken: dipToken)
}
func remove(_ dipToken: String) {
try? backend.remove(dipToken: dipToken)
}
func removeDIPTokens() {
try? backend.removeDIPTokens()
}
}
......@@ -44,6 +44,8 @@ class UserDefaultsStore: PlainStore, ConfigurationAccess {
static let preferredServer = "CurrentRegion" // legacy
static let preferredServerDIPToken = "CurrentRegionDIPToken"
static let pingByServerIdentifier = "PingByServerIdentifier"
static let vpnType = "VPNType"
......@@ -285,10 +287,12 @@ class UserDefaultsStore: PlainStore, ConfigurationAccess {
var preferredServer: Server? {
get {
let identifier = backend.string(forKey: Entries.preferredServer)
return cachedServers.first { $0.identifier == identifier }
let dipToken = preferredServerDIPToken
return cachedServers.first { $0.identifier == identifier && $0.dipToken == dipToken }
}
set {
backend.set(newValue?.identifier, forKey: Entries.preferredServer)
backend.set(newValue?.dipToken, forKey: Entries.preferredServerDIPToken)
var lastServers = historicalServers
if let server = newValue {
......@@ -305,16 +309,15 @@ class UserDefaultsStore: PlainStore, ConfigurationAccess {
}
}
var serverNetwork: ServersNetwork {
var preferredServerDIPToken: String? {
get {
let network = backend.string(forKey: Entries.serverNetwork)
return ServersNetwork(rawValue: network ?? "") ?? ServersNetwork.gen4
return backend.string(forKey: Entries.preferredServerDIPToken)
}
set {
backend.set(newValue.rawValue, forKey: Entries.serverNetwork)
backend.set(preferredServerDIPToken, forKey: Entries.preferredServerDIPToken)
}
}
func ping(forServerIdentifier serverIdentifier: String) -> Int? {
return pingByServerIdentifier[serverIdentifier]
}
......
......@@ -43,6 +43,9 @@ class DefaultServerProvider: ServerProvider, ConfigurationAccess, DatabaseAccess
var historicalServers: [Server] {
get {
if let dipTokens = dipTokens {
return accessedDatabase.plain.historicalServers.filter({$0.dipToken == nil || dipTokens.contains($0.dipToken ?? "")})
}
return accessedDatabase.plain.historicalServers
}
set {
......@@ -76,8 +79,7 @@ class DefaultServerProvider: ServerProvider, ConfigurationAccess, DatabaseAccess
// guard (avg < bestResponseTime) else {