Commit b41ad08c authored by Jose Blaya's avatar Jose Blaya
Browse files

Merge branch 'feature/meta' into develop

parents 37956906 0224769b
Pod::Spec.new do |s|
s.name = "PIALibrary"
s.version = "2.8.0"
s.version = "2.8.1"
s.summary = "PIA client library in Swift."
s.homepage = "https://www.privateinternetaccess.com/"
......
This diff is collapsed.
......@@ -89,14 +89,6 @@ public protocol ServerProvider: class {
- Returns: The found `Server` object or `nil`.
*/
func find(withIdentifier identifier: String) -> Server?
/**
Returns the URL where to find a flag asset associated with a server.
- Parameter server: The `Server` to fetch the flag for.
- Returns: The `URL` of the flag asset. The asset is not guaranteed to be available.
*/
func flagURL(for server: Server) -> URL
/**
Reset the currentServers object
......
......@@ -142,6 +142,9 @@ public class Server: Hashable {
/// - Seealso: `Macros.ping(...)`
public let pingAddress: Address?
/// The meta IP.
public let meta: ServerAddressIP?
var isAutomatic: Bool
/// :nodoc:
......@@ -160,6 +163,7 @@ public class Server: Hashable {
responseTime: Int? = 0,
serverNetwork: ServersNetwork? = .legacy,
geo: Bool = false,
meta: ServerAddressIP? = nil,
regionIdentifier: String) {
self.serial = serial
......@@ -177,6 +181,7 @@ public class Server: Hashable {
self.wireGuardAddressesForUDP = wireGuardAddressesForUDP
self.iKEv2AddressesForUDP = iKEv2AddressesForUDP
self.meta = meta
self.pingAddress = pingAddress
self.serverNetwork = serverNetwork
......
......@@ -62,9 +62,7 @@ protocol WebServices: class {
func downloadRegionsStaticData(_ callback: LibraryCallback<RegionData>?)
func flagURL(for country: String) -> URL
func taskForConnectivityCheck(_ callback: LibraryCallback<ConnectivityStatus>?) -> URLSessionDataTask
func taskForConnectivityCheck(_ callback: LibraryCallback<ConnectivityStatus>?)
func submitDebugLog(_ log: DebugLog, _ callback: SuccessLibraryCallback?)
......
......@@ -561,10 +561,9 @@ class DefaultAccountProvider: AccountProvider, ConfigurationAccess, DatabaseAcce
}
func isAPIEndpointAvailable(_ callback: LibraryCallback<Bool>?) {
let task = webServices.taskForConnectivityCheck { (_, error) in
webServices.taskForConnectivityCheck { (_, error) in
callback?(error == nil, error)
}
task.resume()
}
}
......@@ -77,6 +77,8 @@ public enum ClientError: String, Error {
/// The selected sandbox subscription is not available in production.
case sandboxPurchase
/// Cant retrieve regions
case noRegions
#endif
}
......
......@@ -43,8 +43,6 @@ class ConnectivityDaemon: Daemon, ConfigurationAccess, DatabaseAccess, Preferenc
private var failedConnectivityAttempts: Int
private var pendingConnectivityCheck: URLSessionDataTask?
private var wasConnected: Bool
private init() {
......@@ -57,7 +55,6 @@ class ConnectivityDaemon: Daemon, ConfigurationAccess, DatabaseAccess, Preferenc
isCheckingConnectivity = false
failedConnectivityAttempts = 0
pendingConnectivityCheck = nil
wasConnected = false
}
......@@ -128,8 +125,7 @@ class ConnectivityDaemon: Daemon, ConfigurationAccess, DatabaseAccess, Preferenc
Macros.postNotification(.PIADaemonsDidUpdateConnectivity)
isCheckingConnectivity = true
pendingConnectivityCheck?.cancel()
pendingConnectivityCheck = accessedWebServices.taskForConnectivityCheck { (connectivity, error) in
accessedWebServices.taskForConnectivityCheck { (connectivity, error) in
self.isCheckingConnectivity = false
guard let connectivity = connectivity else {
......@@ -168,7 +164,6 @@ class ConnectivityDaemon: Daemon, ConfigurationAccess, DatabaseAccess, Preferenc
Macros.postNotification(.PIADaemonsDidUpdateConnectivity)
}
pendingConnectivityCheck?.resume()
}
// MARK: Notifications
......
......@@ -71,6 +71,9 @@ class ServersDaemon: Daemon, ConfigurationAccess, DatabaseAccess, ProvidersAcces
self.scheduleServersUpdate(withDelay: pollInterval)
guard let servers = servers else {
if let error = error as? ClientError, error == ClientError.noRegions {
self.pingIfOffline(servers: Client.providers.serverProvider.currentServers)
}
completionBlock(error)
return
}
......@@ -116,6 +119,9 @@ class ServersDaemon: Daemon, ConfigurationAccess, DatabaseAccess, ProvidersAcces
self.scheduleServersUpdate(withDelay: pollInterval)
guard let servers = servers else {
if let error = error as? ClientError, error == ClientError.noRegions {
self.pingIfOffline(servers: Client.providers.serverProvider.currentServers)
}
return
}
self.pingIfOffline(servers: servers)
......
//
// PIAAccountClientStateProvider.swift
// PIALibrary
//
// Created by Jose Blaya on 28/09/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
import PIAAccount
class PIAAccountClientStateProvider : AccountClientStateProvider {
func accountEndpoints() -> [AccountEndpoint] {
let validEndpoints = EndpointManager.shared.availableEndpoints()
var clientEndpoints = [AccountEndpoint]()
for endpoint in validEndpoints.reversed() {
clientEndpoints.append(AccountEndpoint(endpoint: endpoint.host, isProxy: endpoint.isProxy, usePinnedCertificate: endpoint.useCertificatePinning, certificateCommonName: endpoint.commonName))
}
return clientEndpoints
}
}
//
// PIAAccountStagingClientStateProvider.swift
// PIALibrary
//
// Created by Jose Blaya on 28/09/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
import PIAAccount
class PIAAccountStagingClientStateProvider : AccountClientStateProvider {
func accountEndpoints() -> [AccountEndpoint] {
return [
AccountEndpoint(endpoint: Client.configuration.baseUrl, isProxy: false, usePinnedCertificate: false, certificateCommonName: nil),
]
}
}
//
// PIARegionClientStateProvider.swift
// PIALibrary
//
// Created by Jose Blaya on 28/09/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
import Regions
class PIARegionClientStateProvider : RegionClientStateProvider {
func regionEndpoints() -> [RegionEndpoint] {
let validEndpoints = EndpointManager.shared.availableRegionEndpoints()
var clientEndpoints = [RegionEndpoint]()
for endpoint in validEndpoints.reversed() {
clientEndpoints.append(RegionEndpoint(endpoint: endpoint.host, isProxy: endpoint.isProxy, usePinnedCertificate: endpoint.useCertificatePinning, certificateCommonName: endpoint.commonName))
}
return clientEndpoints
}
}
......@@ -128,6 +128,7 @@ class DefaultServerProvider: ServerProvider, ConfigurationAccess, DatabaseAccess
}
if currentServers.isEmpty {
currentServers = bundle.parsed.servers
ServersPinger.shared.ping(withDestinations: currentServers)
}
}
......@@ -170,10 +171,6 @@ class DefaultServerProvider: ServerProvider, ConfigurationAccess, DatabaseAccess
return currentServers.first { $0.identifier == identifier }
}
func flagURL(for server: Server) -> URL {
return webServices.flagURL(for: server.country.lowercased())
}
func resetCurrentServers() {
currentServers = []
}
......@@ -184,14 +181,6 @@ class DefaultServerProvider: ServerProvider, ConfigurationAccess, DatabaseAccess
}
}
extension Server: ProvidersAccess {
/// Shortcut for `ServerProvider.flagURL(for:)` as per `Client.Providers.serverProvider`. Requires `Library` subspec.
public var flagURL: URL {
return accessedProviders.serverProvider.flagURL(for: self)
}
}
extension Server: DatabaseAccess {
/// Returns last ping response in milliseconds. Requires `Library` subspec.
......
......@@ -26,34 +26,6 @@ protocol Endpoint: ConfigurationAccess {
var url: URL { get }
}
enum ClientEndpoint: String, Endpoint {
case signup
case redeem = "giftcard_redeem"
case token = "v2/token"
case account = "v2/account"
case updateAccount = "account"
case logout = "v2/expire_token"
case payment
case status
case ios
case invites
case login_link = "v2/login_link"
var url: URL {
return URL(string: "\(accessedConfiguration.baseUrl)/api/client/\(rawValue)")!
}
}
enum VPNEndpoint: String, Endpoint {
case servers
......@@ -65,11 +37,3 @@ enum VPNEndpoint: String, Endpoint {
}
enum ServerEndpoint: String, Endpoint {
case gen4
var url: URL {
return URL(string: "https://serverlist.piaservers.net/vpninfo/servers/new")!
}
}
//
// EndpointManager.swift
// PIALibrary
//
// Created by Jose Blaya on 15/09/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 struct PinningEndpoint {
let host: String
let isProxy: Bool
let useCertificatePinning: Bool
let commonName: String?
init(host: String, isProxy: Bool = false, useCertificatePinning: Bool = false, commonName: String? = nil) {
self.host = host
self.isProxy = isProxy
self.useCertificatePinning = useCertificatePinning
self.commonName = commonName
}
}
public class EndpointManager {
private let internalUrl = "10.0.0.1"
private let proxy = "piaproxy.net"
private let pia = "www.privateinternetaccess.com"
private let region = "serverlist.piaservers.net"
public static let shared = EndpointManager()
private func availableMetaEndpoints(_ availableEndpoints: inout [PinningEndpoint]) {
var currentServers = Client.providers.serverProvider.currentServers.filter { $0.serverNetwork == .gen4 }
currentServers = currentServers.sorted(by: { $0.pingTime ?? 1000 < $1.pingTime ?? 1000 })
if let historicalServer = Client.providers.serverProvider.historicalServers.first {
availableEndpoints.append(PinningEndpoint(host: historicalServer.meta!.ip,
useCertificatePinning: true,
commonName: historicalServer.meta?.cn))
}
if currentServers.count > 2 {
if currentServers[0].pingTime == nil && currentServers[1].pingTime == nil {
while availableEndpoints.count < 2 {
availableEndpoints.append(PinningEndpoint(host: currentServers.randomElement()!.meta!.ip, useCertificatePinning: true, commonName: currentServers.randomElement()?.meta?.cn))
}
} else {
availableEndpoints.append(PinningEndpoint(host: currentServers[0].meta!.ip, useCertificatePinning: true, commonName: currentServers[0].meta?.cn))
if availableEndpoints.count < 2 {
availableEndpoints.append(PinningEndpoint(host: currentServers[1].meta!.ip, useCertificatePinning: true, commonName: currentServers[1].meta?.cn))
}
}
}
}
public func availableRegionEndpoints() -> [PinningEndpoint] {
if Client.configuration.currentServerNetwork() == .gen4 {
if Client.providers.vpnProvider.isVPNConnected {
return [PinningEndpoint(host: internalUrl),
PinningEndpoint(host: region)]
}
var availableEndpoints = [PinningEndpoint]()
availableMetaEndpoints(&availableEndpoints)
availableEndpoints.append(PinningEndpoint(host: region))
return availableEndpoints
} else {
return [PinningEndpoint(host: region)]
}
}
public func availableEndpoints() -> [PinningEndpoint] {
if Client.configuration.currentServerNetwork() == .gen4 {
if Client.providers.vpnProvider.isVPNConnected {
return [PinningEndpoint(host: internalUrl),
PinningEndpoint(host: pia),
PinningEndpoint(host: proxy, isProxy: true)]
}
var availableEndpoints = [PinningEndpoint]()
availableMetaEndpoints(&availableEndpoints)
availableEndpoints.append(PinningEndpoint(host: pia))
availableEndpoints.append(PinningEndpoint(host: proxy, isProxy: true))
return availableEndpoints
} else {
return [PinningEndpoint(host: pia),
PinningEndpoint(host: proxy, isProxy: true)]
}
}
}
......@@ -48,6 +48,18 @@ class GlossServer: GlossParser {
if Client.configuration.serverNetwork == .gen4 {
var meta: Server.ServerAddressIP?
if let metaServer: [String: Any] = "servers" <~~ json {
if let addressArray: [JSON] = "meta" <~~ metaServer {
for address in addressArray {
if let ip: String = "ip" <~~ address,
let cn: String = "cn" <~~ address {
meta = Server.ServerAddressIP(ip: ip, cn: cn)
}
}
}
}
guard let regionIdentifier: String = "id" <~~ json else {
return nil
}
......@@ -119,6 +131,7 @@ class GlossServer: GlossParser {
responseTime: 0,
serverNetwork: internalServerNetwork ?? .gen4,
geo: geo,
meta: meta,
regionIdentifier: regionIdentifier
)
......
......@@ -24,53 +24,24 @@ import Foundation
import Gloss
extension PIAWebServices {
func flagURL(for country: String) -> URL {
return URL(string: "\(accessedConfiguration.baseUrl)/images/flags/\(country)_3x.png")!
}
func taskForConnectivityCheck(_ callback: ((ConnectivityStatus?, Error?) -> Void)?) -> URLSessionDataTask {
// every status check should use a new HTTP connection (no Keep-Alive), so we
// skip using AFNetworking here and just use NSURLSession directly
let config: URLSessionConfiguration = .ephemeral
config.timeoutIntervalForRequest = Double(accessedConfiguration.connectivityTimeout) / 1000.0
config.timeoutIntervalForResource = Double(accessedConfiguration.connectivityTimeout) / 1000.0
let session = URLSession(configuration: config)
let url = ClientEndpoint.status.url
return session.dataTask(with: url, completionHandler: { (data, response, error) in
DispatchQueue.main.async {
if let error = error {
callback?(nil, error)
return
}
guard let httpResponse = response as? HTTPURLResponse, let data = data else {
callback?(nil, ClientError.malformedResponseData)
return
}
let statusCode = httpResponse.statusCode
guard (statusCode == 200) else {
callback?(nil, ClientError.unexpectedReply)
return
}
let jsonObject: Any
do {
jsonObject = try JSONSerialization.jsonObject(with: data, options: [])
} catch let e {
callback?(nil, e)
func taskForConnectivityCheck(_ callback: ((ConnectivityStatus?, Error?) -> Void)?) {
self.accountAPI.clientStatus { (information, error) in
DispatchQueue.main.async {
if let _ = error {
callback?(nil, ClientError.internetUnreachable)
return
}
guard let json = jsonObject as? JSON, let connectivity = GlossConnectivityStatus(json: json) else {
if let information = information {
callback?(ConnectivityStatus(ipAddress: information.ip, isVPN: information.connected), nil)
} else {
callback?(nil, ClientError.malformedResponseData)
return
}
callback?(connectivity.parsed, nil)
}
})
}
}
func submitDebugLog(_ log: DebugLog, _ callback: SuccessLibraryCallback?) {
......
......@@ -26,6 +26,7 @@ import Gloss
import SwiftyBeaver
import PIARegions
import PIAAccount
import Regions
private let log = SwiftyBeaver.self
......@@ -34,17 +35,18 @@ class PIAWebServices: WebServices, ConfigurationAccess {
private static let serversVersion = 1002
private static let store = "apple_app_store"
private let regionsTask = RegionsTask()
private let accountAPI: IOSAccountAPI!
private let regionsTask = RegionsTask(stateProvider: PIARegionClientStateProvider())
let accountAPI: IOSAccountAPI!
init() {
if Client.environment == .staging {
self.accountAPI = AccountBuilder().setPlatform(platform: .ios)
.setStagingEndpoint(stagingEndpoint: Client.configuration.baseUrl)
self.accountAPI = AccountBuilder<IOSAccountAPI>().setPlatform(platform: .ios)
.setClientStateProvider(clientStateProvider: PIAAccountStagingClientStateProvider())
.setUserAgentValue(userAgentValue: userAgent).build() as? IOSAccountAPI
} else {
self.accountAPI = AccountBuilder().setPlatform(platform: .ios)
.setUserAgentValue(userAgentValue: userAgent).build() as? IOSAccountAPI
self.accountAPI = AccountBuilder<IOSAccountAPI>().setPlatform(platform: .ios)
.setClientStateProvider(clientStateProvider: PIAAccountClientStateProvider())
.setUserAgentValue(userAgentValue: userAgent).build() as? IOSAccountAPI
}
}
......@@ -308,7 +310,7 @@ class PIAWebServices: WebServices, ConfigurationAccess {
self.regionsTask.fetch { response, jsonResponse, error in
if let error = error {
callback?(nil, ClientError.malformedResponseData)
callback?(nil, ClientError.noRegions)
return
}