summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAndrej Mihajlov <and@mullvad.net>2019-12-04 12:41:32 +0100
committerAndrej Mihajlov <and@mullvad.net>2019-12-04 12:41:32 +0100
commit96cca7d964a21ada0f7b3bc027290aa63acb9eac (patch)
treea0026b03f2a3d5705ad477a8ed5f3de4ce0e5ee2
parent649a06876adfd44333ae2b9b17225408db1a8f39 (diff)
parent6df274ba8c0c5aa8cd67595c1b76cffac4a6c177 (diff)
downloadmullvadvpn-96cca7d964a21ada0f7b3bc027290aa63acb9eac.tar.xz
mullvadvpn-96cca7d964a21ada0f7b3bc027290aa63acb9eac.zip
Merge branch 'mullvadapi-combine'
-rw-r--r--ios/MullvadVPN/JsonRequestProcedure.swift46
-rw-r--r--ios/MullvadVPN/MullvadAPI.swift164
-rw-r--r--ios/MullvadVPN/RelayList.swift70
3 files changed, 173 insertions, 107 deletions
diff --git a/ios/MullvadVPN/JsonRequestProcedure.swift b/ios/MullvadVPN/JsonRequestProcedure.swift
deleted file mode 100644
index 762bb1f1b3..0000000000
--- a/ios/MullvadVPN/JsonRequestProcedure.swift
+++ /dev/null
@@ -1,46 +0,0 @@
-//
-// JsonRequestProcedure.swift
-// MullvadVPN
-//
-// Created by pronebird on 14/05/2019.
-// Copyright © 2019 Amagicom AB. All rights reserved.
-//
-
-import Foundation
-import ProcedureKit
-
-final class JsonRequestProcedure<Input, Output: Decodable>: GroupProcedure, InputProcedure, OutputProcedure {
-
- typealias URLRequestBuilder = (Input) throws -> URLRequest
-
- var input: Pending<Input>
- var output: Pending<ProcedureResult<Output>> = .pending
-
- init(dispatchQueue underlyingQueue: DispatchQueue? = nil, input: Input? = nil, requestBuilder: @escaping URLRequestBuilder) {
- self.input = input.flatMap { .ready($0) } ?? .pending
-
- let createRequest = TransformProcedure { try requestBuilder($0) }
- createRequest.input = self.input
-
- let networkRequest = NetworkProcedure {
- NetworkDataProcedure(session: URLSession.shared)
- }.injectResult(from: createRequest)
-
- let payloadParsing = DecodeJSONProcedure<Output>(
- dateDecodingStrategy: .iso8601,
- dataDecodingStrategy: .base64,
- keyDecodingStrategy: .convertFromSnakeCase
- ).injectPayload(fromNetwork: networkRequest)
-
- super.init(dispatchQueue: underlyingQueue, operations: [createRequest, networkRequest, payloadParsing])
-
- bind(from: payloadParsing)
- bindAndNotifySetInputReady(to: createRequest)
- }
-}
-
-extension JsonRequestProcedure where Input == Void {
- convenience init(requestBuilder: @escaping URLRequestBuilder) {
- self.init(input: (), requestBuilder: requestBuilder)
- }
-}
diff --git a/ios/MullvadVPN/MullvadAPI.swift b/ios/MullvadVPN/MullvadAPI.swift
index fe2160af68..ebe8956830 100644
--- a/ios/MullvadVPN/MullvadAPI.swift
+++ b/ios/MullvadVPN/MullvadAPI.swift
@@ -8,69 +8,149 @@
import Foundation
import Network
-import ProcedureKit
+import Combine
+/// API server URL
private let kMullvadAPIURL = URL(string: "https://api.mullvad.net/rpc/")!
+/// Network request timeout in seconds
+private let kNetworkTimeout: TimeInterval = 10
+
+/// An error type emitted by `MullvadAPI`
+enum MullvadAPIError: Error {
+ /// A network communication error
+ case network(URLError)
+
+ /// An error occured when decoding the JSON response
+ case decoding(Error)
+
+ /// An error occured when encoding the JSON request
+ case encoding(Error)
+}
+
+/// A type that describes the account verification result
+enum AccountVerification {
+ /// The app should attempt to verify the account token at some point later because the network
+ /// may not be available at this time.
+ case deferred(DeferReasonError)
+
+ /// The app successfully verified the account token with the server
+ case verified(Date)
+
+ // Invalid token
+ case invalid
+}
+
+/// An error type that describes why the account verification was deferred
+enum DeferReasonError: Error {
+ /// Mullvad API communication error
+ case communication(MullvadAPIError)
+
+ /// Mullvad API responded with an error
+ case server(JsonRpcResponseError)
+}
+
+/// The error code returned by the API when it cannot find the given account token
+private let kAccountDoesNotExistErrorCode = -200
+
class MullvadAPI {
+ private let session: URLSession
- struct WireguardKeyRequest: Codable {
- var accountToken: String
- var publicKey: Data
+ init(session: URLSession = URLSession.shared) {
+ self.session = session
}
- class func getRelayList() -> JsonRequestProcedure<Void, JsonRpcResponse<RelayList>> {
- return JsonRequestProcedure(requestBuilder: {
- try makeURLRequest(method: "POST",
- rpcRequest: JsonRpcRequest(method: "relay_list_v2", params: []))
- })
+ func getRelayList() -> AnyPublisher<JsonRpcResponse<RelayList>, MullvadAPIError> {
+ let request = JsonRpcRequest(method: "relay_list_v3", params: [])
+
+ return MullvadAPI.makeDataTaskPublisher(request: request)
}
- class func getAccountExpiry(accountToken: String? = nil) -> JsonRequestProcedure<String, JsonRpcResponse<Date>> {
- return JsonRequestProcedure(input: accountToken, requestBuilder: {
- try makeURLRequest(
- method: "POST",
- rpcRequest: JsonRpcRequest(method: "get_expiry", params: [AnyEncodable($0)])
- )
- })
+ func getAccountExpiry(accountToken: String) -> AnyPublisher<JsonRpcResponse<Date>, MullvadAPIError> {
+ let request = JsonRpcRequest(method: "get_expiry", params: [AnyEncodable(accountToken)])
+
+ return MullvadAPI.makeDataTaskPublisher(request: request)
}
- class func verifyAccountToken(_ accountToken: String? = nil) -> AccountVerificationProcedure {
- return AccountVerificationProcedure(accountToken: accountToken)
+ func verifyAccount(accountToken: String) -> AnyPublisher<AccountVerification, Never> {
+ return getAccountExpiry(accountToken: accountToken)
+ .map({ (response) -> AccountVerification in
+ switch response.result {
+ case .success(let expiry):
+ // Report .verified when expiry is successfully received
+ return .verified(expiry)
+
+ case .failure(let serverError):
+ if serverError.code == kAccountDoesNotExistErrorCode {
+ // Report .invalid account if the server responds with the special code
+ return .invalid
+ } else {
+ // Otherwise report .deferred and pass the server error along
+ return .deferred(.server(serverError))
+ }
+ }
+ })
+ .catch({ (networkError) in
+ // Treat all network errors as .deferred verification
+ return Just(.deferred(.communication(networkError)))
+ })
+ .eraseToAnyPublisher()
}
- class func pushWireguardKey(_ pushRequest: WireguardKeyRequest? = nil) -> JsonRequestProcedure<WireguardKeyRequest, JsonRpcResponse<WireguardAssociatedAddresses>> {
- return JsonRequestProcedure(input: pushRequest, requestBuilder: { (input) -> URLRequest in
- let rpcRequest = JsonRpcRequest(method: "push_wg_key", params: [
- AnyEncodable(input.accountToken),
- AnyEncodable(input.publicKey)
- ])
- return try makeURLRequest(method: "POST", rpcRequest: rpcRequest)
- })
+ func pushWireguardKey(accountToken: String, publicKey: Data) -> AnyPublisher<JsonRpcResponse<WireguardAssociatedAddresses>, MullvadAPIError> {
+ let request = JsonRpcRequest(method: "push_wg_key", params: [
+ AnyEncodable(accountToken),
+ AnyEncodable(publicKey)
+ ])
+
+ return MullvadAPI.makeDataTaskPublisher(request: request)
+ }
+
+ func checkWireguardKey(accountToken: String, publicKey: Data) -> AnyPublisher<JsonRpcResponse<WireguardAssociatedAddresses>, MullvadAPIError> {
+ let request = JsonRpcRequest(method: "check_wg_key", params: [
+ AnyEncodable(accountToken),
+ AnyEncodable(publicKey)
+ ])
+
+ return MullvadAPI.makeDataTaskPublisher(request: request)
}
- class func checkWireguardKey(_ pushRequest: WireguardKeyRequest? = nil) -> JsonRequestProcedure<WireguardKeyRequest, JsonRpcResponse<Bool>> {
- return JsonRequestProcedure(input: pushRequest, requestBuilder: { (input) -> URLRequest in
- let rpcRequest = JsonRpcRequest(method: "check_wg_key", params: [
- AnyEncodable(input.accountToken),
- AnyEncodable(input.publicKey)
- ])
- return try makeURLRequest(method: "POST", rpcRequest: rpcRequest)
- })
+ private static func makeDataTaskPublisher<T: Decodable>(request: JsonRpcRequest) -> AnyPublisher<JsonRpcResponse<T>, MullvadAPIError> {
+ return Just(request)
+ .encode(encoder: makeJSONEncoder())
+ .mapError { MullvadAPIError.encoding($0) }
+ .map { self.makeURLRequest(httpBody: $0) }
+ .flatMap {
+ URLSession.shared.dataTaskPublisher(for: $0)
+ .mapError { MullvadAPIError.network($0) }
+ .map { $0.data }
+ .decode(type: JsonRpcResponse<T>.self, decoder: makeJSONDecoder())
+ .mapError { MullvadAPIError.decoding($0) }
+ }.eraseToAnyPublisher()
}
- private class func makeURLRequest(method: String, rpcRequest: JsonRpcRequest) throws -> URLRequest {
+ private static func makeURLRequest(httpBody: Data) -> URLRequest {
+ var request = URLRequest(url: kMullvadAPIURL, cachePolicy: .useProtocolCachePolicy, timeoutInterval: kNetworkTimeout)
+ request.addValue("application/json", forHTTPHeaderField: "Content-Type")
+ request.httpMethod = "POST"
+ request.httpBody = httpBody
+
+ return request
+ }
+
+ private static func makeJSONEncoder() -> JSONEncoder {
let encoder = JSONEncoder()
encoder.keyEncodingStrategy = .convertToSnakeCase
encoder.dateEncodingStrategy = .iso8601
encoder.dataEncodingStrategy = .base64
-
- var urlRequest = URLRequest(url: kMullvadAPIURL)
- urlRequest.httpMethod = method
- urlRequest.httpBody = try encoder.encode(rpcRequest)
- urlRequest.addValue("application/json", forHTTPHeaderField: "Content-Type")
-
- return urlRequest
+ return encoder
}
+ private static func makeJSONDecoder() -> JSONDecoder {
+ let decoder = JSONDecoder()
+ decoder.keyDecodingStrategy = .convertFromSnakeCase
+ decoder.dateDecodingStrategy = .iso8601
+ decoder.dataDecodingStrategy = .base64
+ return decoder
+ }
}
diff --git a/ios/MullvadVPN/RelayList.swift b/ios/MullvadVPN/RelayList.swift
index 8601572af8..fc892cd269 100644
--- a/ios/MullvadVPN/RelayList.swift
+++ b/ios/MullvadVPN/RelayList.swift
@@ -12,37 +12,69 @@ import Network
struct RelayList: Codable {
struct Country: Codable {
- let name: String
- let code: String
- let cities: [City]
+ var name: String
+ var code: String
+ var cities: [City]
}
struct City: Codable {
- let name: String
- let code: String
- let latitude: Double
- let longitude: Double
- let relays: [Hostname]
+ var name: String
+ var code: String
+ var latitude: Double
+ var longitude: Double
+ var relays: [Hostname]
}
struct Hostname: Codable {
- let hostname: String
- let ipv4AddrIn: IPv4Address
- let includeInCountry: Bool
- let weight: Int32
- let tunnels: Tunnels?
+ var hostname: String
+ var ipv4AddrIn: IPv4Address
+ var includeInCountry: Bool
+ var active: Bool
+ var weight: Int32
+ var tunnels: Tunnels?
}
struct Tunnels: Codable {
- let wireguard: [WireguardTunnel]?
+ var wireguard: [WireguardTunnel]?
}
struct WireguardTunnel: Codable {
- let ipv4Gateway: IPv4Address
- let ipv6Gateway: IPv6Address
- let publicKey: Data
- let portRanges: [ClosedRange<UInt16>]
+ var ipv4Gateway: IPv4Address
+ var ipv6Gateway: IPv6Address
+ var publicKey: Data
+ var portRanges: [ClosedRange<UInt16>]
+ }
+
+ var countries: [Country]
+}
+
+extension RelayList {
+
+ /// Returns an alphabetically sorted `RelayList`
+ func sorted() -> Self {
+ let lexicalComparator = { (a: String, b: String) -> Bool in
+ return a.localizedCaseInsensitiveCompare(b) == .orderedAscending
+ }
+
+ let fileComparator = { (a: String, b: String) -> Bool in
+ return a.localizedStandardCompare(b) == .orderedAscending
+ }
+
+ let sortedCountries = countries
+ .sorted { lexicalComparator($0.name, $1.name) }
+ .map { (country) -> RelayList.Country in
+ var sortedCountry = country
+ sortedCountry.cities = country.cities.sorted { lexicalComparator($0.name, $1.name) }
+ .map({ (city) -> RelayList.City in
+ var sortedCity = city
+ sortedCity.relays = city.relays
+ .sorted { fileComparator($0.hostname, $1.hostname) }
+ return sortedCity
+ })
+ return sortedCountry
+ }
+
+ return RelayList(countries: sortedCountries)
}
- let countries: [Country]
}