Skip to content

Commit

Permalink
PIA-1845: Add Dedicated IP native endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
kp-said-rehouni committed Jul 3, 2024
1 parent 0f0daed commit 2b8ddeb
Show file tree
Hide file tree
Showing 24 changed files with 818 additions and 112 deletions.
3 changes: 3 additions & 0 deletions Sources/PIALibrary/Account/Data/ClientErrorMapper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ struct ClientErrorMapper {

case .unableToDecodeData:
return .malformedResponseData

case .unauthorized:
return .unauthorized
}
}

Expand Down
46 changes: 46 additions & 0 deletions Sources/PIALibrary/Account/Data/DedicatedIPServerMapper.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@

import Foundation

class DedicatedIPServerMapper: DedicatedIPServerMapperType {
private let dedicatedIPTokenHandler: DedicatedIPTokenHandlerType

init(dedicatedIPTokenHandler: DedicatedIPTokenHandlerType) {
self.dedicatedIPTokenHandler = dedicatedIPTokenHandler
}

func map(dedicatedIps: [DedicatedIPInformation]) -> Result<[Server], ClientError> {
var dipRegions = [Server]()

for dipServer in dedicatedIps {
let status = DedicatedIPStatus(fromAPIStatus: dipServer.status)

switch dipServer.status {
case .active:

guard let firstServer = Client.providers.serverProvider.currentServers.first(where: {$0.regionIdentifier == dipServer.id}) else {
return .failure(ClientError.malformedResponseData)
}

guard let ip = dipServer.ip, let cn = dipServer.cn, let expirationTime = dipServer.dipExpire else {
return .failure(ClientError.malformedResponseData)
}

let dipUsername = "dedicated_ip_"+dipServer.dipToken+"_"+String.random(length: 8)
let expiringDate = Date(timeIntervalSince1970: TimeInterval(expirationTime))
let server = Server.ServerAddressIP(ip: ip, cn: cn, van: false)

let dipRegion = Server(serial: firstServer.serial, name: firstServer.name, country: firstServer.country, hostname: firstServer.hostname, openVPNAddressesForTCP: [server], openVPNAddressesForUDP: [server], wireGuardAddressesForUDP: [server], iKEv2AddressesForUDP: [server], pingAddress: firstServer.pingAddress, geo: false, meta: nil, dipExpire: expiringDate, dipToken: dipServer.dipToken, dipStatus: status, dipUsername: dipUsername, regionIdentifier: firstServer.regionIdentifier)

dipRegions.append(dipRegion)
dedicatedIPTokenHandler(dedicatedIp: dipServer, dipUsername: dipUsername)

default:

let dipRegion = Server(serial: "", name: "", country: "", hostname: "", openVPNAddressesForTCP: [], openVPNAddressesForUDP: [], wireGuardAddressesForUDP: [], iKEv2AddressesForUDP: [], pingAddress: nil, geo: false, meta: nil, dipExpire: nil, dipToken: nil, dipStatus: status, dipUsername: nil, regionIdentifier: "")
dipRegions.append(dipRegion)
}
}

return .success(dipRegions)
}
}
21 changes: 21 additions & 0 deletions Sources/PIALibrary/Account/Data/DedicatedIPTokenHandler.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@

import Foundation

class DedicatedIPTokenHandler: DedicatedIPTokenHandlerType {
private let secureStore: SecureStore

init(secureStore: SecureStore) {
self.secureStore = secureStore
}

func callAsFunction(dedicatedIp: DedicatedIPInformation, dipUsername: String) {
if dedicatedIp.isAboutToExpire {
Macros.postNotification(.PIADIPRegionExpiring, [.token : dedicatedIp.dipToken])
}

Macros.postNotification(.PIADIPCheckIP, [.token : dedicatedIp.dipToken, .ip : dedicatedIp.ip!])

secureStore.setDIPToken(dedicatedIp.dipToken)
secureStore.setPassword(dedicatedIp.ip!, forDipToken: dipUsername)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@

import Foundation
import NWHttpConnection

struct GetDedicatedIPsRequestConfiguration: NetworkRequestConfigurationType {
let networkRequestModule: NetworkRequestModule = .account
let path: RequestAPI.Path = .dedicatedIp
let httpMethod: NWHttpConnection.NWConnectionHTTPMethod = .post
let contentType: NetworkRequestContentType = .json
var inlcudeAuthHeaders: Bool = true
var urlQueryParameters: [String : String]? = nil
let responseDataType: NWDataResponseType = .jsonData

var body: Data? = nil
var otherHeaders: [String : String]? = nil

let timeout: TimeInterval = 10
let requestQueue: DispatchQueue? = DispatchQueue(label: "getDedicatedIPs_request.queue")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@

import Foundation
import NWHttpConnection

struct RenewDedicatedIPRequestConfiguration: NetworkRequestConfigurationType {
let networkRequestModule: NetworkRequestModule = .account
let path: RequestAPI.Path = .renewDedicatedIp
let httpMethod: NWHttpConnection.NWConnectionHTTPMethod = .post
let contentType: NetworkRequestContentType = .json
var inlcudeAuthHeaders: Bool = true
var urlQueryParameters: [String : String]? = nil
let responseDataType: NWDataResponseType = .jsonData

var body: Data? = nil
var otherHeaders: [String : String]? = nil

let timeout: TimeInterval = 10
let requestQueue: DispatchQueue? = DispatchQueue(label: "RenewDedicatedIP_request.queue")
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,31 +6,11 @@ class SignupInformationDataCoverter: SignupInformationDataCoverterType {
let signupInformation = SignupInformation(store: "apple_app_store",
receipt: signup.receipt.base64EncodedString(),
email: signup.email,
marketing: stringify(json: signup.marketing),
debug: stringify(json: signup.debug))
marketing: stringify(json: signup.marketing, prettyPrinted: false),
debug: stringify(json: signup.debug, prettyPrinted: false))

return signupInformation.toData()
}

private func stringify(json: [String: Any]?, prettyPrinted: Bool = false) -> String? {
guard let json else {
return nil
}

var options: JSONSerialization.WritingOptions = []
if prettyPrinted {
options = JSONSerialization.WritingOptions.prettyPrinted
}

do {
let data = try JSONSerialization.data(withJSONObject: json, options: options)
if let string = String(data: data, encoding: String.Encoding.utf8) {
return string
}
} catch {
print(error)
}

return nil
}
}

extension SignupInformationDataCoverter: JSONToStringCoverterType {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@

import Foundation

struct DedicatedIPInformationResult: Codable {
let result: [DedicatedIPInformation]
}

public struct DedicatedIPInformation: Codable {
enum Status: String, Codable {
case active, expired, invalid, error
}

let id: String?
let ip: String?
let cn: String?
let groups: [String]?
let dipExpire: Double?
let dipToken: String
let status: DedicatedIPInformation.Status

enum CodingKeys: String, CodingKey {
case id = "id"
case ip = "ip"
case cn = "cn"
case groups = "groups"
case dipExpire = "dip_expire"
case dipToken = "dip_token"
case status = "status"
}

//Expiring in 5 days or less
var isAboutToExpire: Bool {
guard let dipExpire, let nextDays = Calendar.current.date(byAdding: .day, value: 5, to: Date())
else {
return true
}

let expiringDate = Date(timeIntervalSince1970: TimeInterval(dipExpire))
return nextDays >= expiringDate
}

static func makeWith(data: Data) -> [DedicatedIPInformation]? {
let dto = try? JSONDecoder().decode(DedicatedIPInformationResult.self, from: data)
return dto?.result
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@

import Foundation

protocol DedicatedIPServerMapperType {
func map(dedicatedIps: [DedicatedIPInformation]) -> Result<[Server], ClientError>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@

import Foundation

protocol DedicatedIPTokenHandlerType {
func callAsFunction(dedicatedIp: DedicatedIPInformation, dipUsername: String)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@

import Foundation

public protocol GetDedicatedIPsUseCaseType {
typealias Completion = ((Result<[DedicatedIPInformation], NetworkRequestError>) -> Void)
func callAsFunction(dipTokens: [String], completion: @escaping Completion)
}

class GetDedicatedIPsUseCase: GetDedicatedIPsUseCaseType {
private let networkClient: NetworkRequestClientType
private let refreshAuthTokensChecker: RefreshAuthTokensCheckerType

init(networkClient: NetworkRequestClientType, refreshAuthTokensChecker: RefreshAuthTokensCheckerType) {
self.networkClient = networkClient
self.refreshAuthTokensChecker = refreshAuthTokensChecker
}

func callAsFunction(dipTokens: [String], completion: @escaping Completion) {
refreshAuthTokensChecker.refreshIfNeeded { [weak self] error in
guard let self else { return }
if let error {
completion(.failure(error))
} else {
networkClient.executeRequest(with: makeConfiguration(dipTokens: dipTokens)) { error, dataResponse in
if let error {
self.handleErrorResponse(error, completion: completion)
} else if let dataResponse {
self.handleDataResponse(dataResponse, completion: completion)
} else {
completion(.failure(NetworkRequestError.allConnectionAttemptsFailed()))
}
}
}
}
}

private func makeConfiguration(dipTokens: [String]) -> GetDedicatedIPsRequestConfiguration {
var configuration = GetDedicatedIPsRequestConfiguration()

let bodyDataDict = ["tokens": dipTokens]

if let bodyData = try? JSONEncoder().encode(bodyDataDict) {
configuration.body = bodyData
}

return configuration
}

private func handleErrorResponse(_ error: NetworkRequestError, completion: @escaping GetDedicatedIPsUseCaseType.Completion) {
switch error {
case .allConnectionAttemptsFailed(let statusCode):
completion(.failure(statusCode == 401 ? NetworkRequestError.unauthorized : error))
return
case .connectionError(statusCode: let statusCode, message: _):
completion(.failure(statusCode == 401 ? NetworkRequestError.unauthorized : error))
return
default:
completion(.failure(error))
}
}

private func handleDataResponse(_ dataResponse: NetworkRequestResponseType, completion: @escaping GetDedicatedIPsUseCaseType.Completion) {
guard let dataResponseContent = dataResponse.data else {
completion(.failure(NetworkRequestError.noDataContent))
return
}

guard let dto = DedicatedIPInformation.makeWith(data: dataResponseContent) else {
completion(.failure(NetworkRequestError.unableToDecodeDataContent))
return
}

completion(.success(dto))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@

import Foundation

public protocol RenewDedicatedIPUseCaseType {
typealias Completion = ((Result<Void, NetworkRequestError>) -> Void)
func callAsFunction(dipToken: String, completion: @escaping Completion)
}

class RenewDedicatedIPUseCase: RenewDedicatedIPUseCaseType {
private let networkClient: NetworkRequestClientType
private let refreshAuthTokensChecker: RefreshAuthTokensCheckerType

init(networkClient: NetworkRequestClientType, refreshAuthTokensChecker: RefreshAuthTokensCheckerType) {
self.networkClient = networkClient
self.refreshAuthTokensChecker = refreshAuthTokensChecker
}

func callAsFunction(dipToken: String, completion: @escaping Completion) {
refreshAuthTokensChecker.refreshIfNeeded { [weak self] error in
guard let self else { return }
if let error {
completion(.failure(error))
} else {
networkClient.executeRequest(with: makeConfiguration(dipToken: dipToken)) { error, response in
if let error {
self.handleErrorResponse(error, completion: completion)
return
}

completion(.success(()))
}
}
}
}

private func makeConfiguration(dipToken: String) -> RenewDedicatedIPRequestConfiguration {
var configuration = RenewDedicatedIPRequestConfiguration()

let bodyDataDict = ["token": dipToken]

if let bodyData = try? JSONEncoder().encode(bodyDataDict) {
configuration.body = bodyData
}

return configuration
}

private func handleErrorResponse(_ error: NetworkRequestError, completion: @escaping RenewDedicatedIPUseCaseType.Completion) {
switch error {
case .allConnectionAttemptsFailed(let statusCode):
completion(.failure(statusCode == 401 ? NetworkRequestError.unauthorized : error))
return
case .connectionError(statusCode: let statusCode, message: let message):
completion(.failure(statusCode == 401 ? NetworkRequestError.unauthorized : error))
return
default:
completion(.failure(error))
}
}
}
2 changes: 1 addition & 1 deletion Sources/PIALibrary/Client+Providers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ extension Client {
public var accountProvider: AccountProvider = AccountFactory.makeDefaultAccountProvider()

/// Provides methods for handling the available VPN servers.
public var serverProvider: ServerProvider = DefaultServerProvider()
public var serverProvider: ServerProvider = ServerProviderFactory.makeDefaultServerProvider()

/// Provides methods for controlling the VPN connection.
public var vpnProvider: VPNProvider = DefaultVPNProvider()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public enum NetworkRequestError: Error, Equatable {
case unableToDecodeDataContent
case connectionCompletedWithNoResponse
case badReceipt
case unauthorized
case unknown(message: String? = nil)
case unableToDecodeData

Expand Down
8 changes: 8 additions & 0 deletions Sources/PIALibrary/Mock/MockDedicatedIPServerMapper.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@

import Foundation

class MockDedicatedIPServerMapper: DedicatedIPServerMapperType {
func map(dedicatedIps: [DedicatedIPInformation]) -> Result<[Server], ClientError> {
return .success([])
}
}
6 changes: 6 additions & 0 deletions Sources/PIALibrary/Mock/MockGetDedicatedIPsUseCase.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@

import Foundation

class MockGetDedicatedIPsUseCase: GetDedicatedIPsUseCaseType {
func callAsFunction(dipTokens: [String], completion: @escaping Completion) {}
}
6 changes: 6 additions & 0 deletions Sources/PIALibrary/Mock/MockRenewDedicatedIPUseCase.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@

import Foundation

class MockRenewDedicatedIPUseCase: RenewDedicatedIPUseCaseType {
func callAsFunction(dipToken: String, completion: @escaping Completion) {}
}
Loading

0 comments on commit 2b8ddeb

Please sign in to comment.