diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2019-12-04 12:41:32 +0100 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2019-12-04 12:41:32 +0100 |
| commit | 96cca7d964a21ada0f7b3bc027290aa63acb9eac (patch) | |
| tree | a0026b03f2a3d5705ad477a8ed5f3de4ce0e5ee2 | |
| parent | 649a06876adfd44333ae2b9b17225408db1a8f39 (diff) | |
| parent | 6df274ba8c0c5aa8cd67595c1b76cffac4a6c177 (diff) | |
| download | mullvadvpn-96cca7d964a21ada0f7b3bc027290aa63acb9eac.tar.xz mullvadvpn-96cca7d964a21ada0f7b3bc027290aa63acb9eac.zip | |
Merge branch 'mullvadapi-combine'
| -rw-r--r-- | ios/MullvadVPN/JsonRequestProcedure.swift | 46 | ||||
| -rw-r--r-- | ios/MullvadVPN/MullvadAPI.swift | 164 | ||||
| -rw-r--r-- | ios/MullvadVPN/RelayList.swift | 70 |
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] } |
