diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2020-07-07 18:28:22 +0200 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2020-07-07 18:28:22 +0200 |
| commit | 8443875d3a7098082a1df1d882dc8908fe43e30a (patch) | |
| tree | 2b49b7c0793b69fb87d1d3b1b712d1dee4f222ba | |
| parent | 419dfade7c35b988ff575764a32bd0b51fd4738a (diff) | |
| download | mullvadvpn-8443875d3a7098082a1df1d882dc8908fe43e30a.tar.xz mullvadvpn-8443875d3a7098082a1df1d882dc8908fe43e30a.zip | |
Uncombine MullvadRpc
| -rw-r--r-- | ios/MullvadVPN/MullvadRpc.swift | 241 |
1 files changed, 167 insertions, 74 deletions
diff --git a/ios/MullvadVPN/MullvadRpc.swift b/ios/MullvadVPN/MullvadRpc.swift index 95f1d36683..59c53348fe 100644 --- a/ios/MullvadVPN/MullvadRpc.swift +++ b/ios/MullvadVPN/MullvadRpc.swift @@ -8,7 +8,6 @@ import Foundation import Network -import Combine /// API server URL private let kMullvadAPIURL = URL(string: "https://api.mullvad.net/rpc/")! @@ -73,7 +72,7 @@ class MullvadRpc { } /// An error type emitted by `MullvadRpc` - enum Error: Swift.Error { + enum Error: ChainedError { /// A network communication error case network(URLError) @@ -86,19 +85,19 @@ class MullvadRpc { /// An error occured when encoding the JSON request case encoding(Swift.Error) - var localizedDescription: String { + var errorDescription: String? { switch self { - case .network(let urlError): - return "Network error: \(urlError.localizedDescription)" + case .network: + return "Network error" - case .server(let serverError): - return "Server error: \(serverError.localizedDescription)" + case .server: + return "Server error" - case .encoding(let encodingError): - return "Encoding error: \(encodingError.localizedDescription)" + case .encoding: + return "Encoding error" - case .decoding(let decodingError): - return "Decoding error: \(decodingError.localizedDescription)" + case .decoding: + return "Decoding error" } } } @@ -108,119 +107,96 @@ class MullvadRpc { return MullvadRpc(session: URLSession(configuration: .ephemeral)) } + class func makeJSONEncoder() -> JSONEncoder { + let encoder = JSONEncoder() + encoder.keyEncodingStrategy = .convertToSnakeCase + encoder.dateEncodingStrategy = .iso8601 + encoder.dataEncodingStrategy = .base64 + return encoder + } + + class func makeJSONDecoder() -> JSONDecoder { + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + decoder.dateDecodingStrategy = .iso8601 + decoder.dataDecodingStrategy = .base64 + return decoder + } + init(session: URLSession) { self.session = session } - func createAccount() -> AnyPublisher<String, MullvadRpc.Error> { + func createAccount() -> MullvadRpc.Request<String> { let request = JsonRpcRequest(method: "create_account", params: []) - return makeDataTaskPublisher(request: request) + return MullvadRpc.Request(session: session, request: request) } - func getRelayList() -> AnyPublisher<RelayList, MullvadRpc.Error> { + func getRelayList() -> MullvadRpc.Request<RelayList> { let request = JsonRpcRequest(method: "relay_list_v3", params: []) - return makeDataTaskPublisher(request: request) + return MullvadRpc.Request(session: session, request: request) } - func getAccountExpiry(accountToken: String) -> AnyPublisher<Date, MullvadRpc.Error> { + func getAccountExpiry(accountToken: String) -> MullvadRpc.Request<Date> { let request = JsonRpcRequest(method: "get_expiry", params: [AnyEncodable(accountToken)]) - return makeDataTaskPublisher(request: request) + return MullvadRpc.Request(session: session, request: request) + } + + func getAccountExpiry(request: MullvadRpc.Request<Date>? = nil) -> MullvadRpc.Operation<Date> { + return MullvadRpc.Operation(request: request) } - func pushWireguardKey(accountToken: String, publicKey: Data) -> AnyPublisher<WireguardAssociatedAddresses, MullvadRpc.Error> { + func pushWireguardKey(accountToken: String, publicKey: Data) -> MullvadRpc.Request<WireguardAssociatedAddresses> { let request = JsonRpcRequest(method: "push_wg_key", params: [ AnyEncodable(accountToken), AnyEncodable(publicKey) ]) - return makeDataTaskPublisher(request: request) + return MullvadRpc.Request(session: session, request: request) } - func replaceWireguardKey(accountToken: String, oldPublicKey: Data, newPublicKey: Data) -> AnyPublisher<WireguardAssociatedAddresses, MullvadRpc.Error> { + func replaceWireguardKey(accountToken: String, oldPublicKey: Data, newPublicKey: Data) -> MullvadRpc.Request<WireguardAssociatedAddresses> { let request = JsonRpcRequest(method: "replace_wg_key", params: [ AnyEncodable(accountToken), AnyEncodable(oldPublicKey), AnyEncodable(newPublicKey) ]) - return makeDataTaskPublisher(request: request) + return MullvadRpc.Request(session: session, request: request) } - func checkWireguardKey(accountToken: String, publicKey: Data) -> AnyPublisher<Bool, MullvadRpc.Error> { + func checkWireguardKey(accountToken: String, publicKey: Data) -> MullvadRpc.Request<Bool> { let request = JsonRpcRequest(method: "check_wg_key", params: [ AnyEncodable(accountToken), AnyEncodable(publicKey) ]) - return makeDataTaskPublisher(request: request) + return MullvadRpc.Request(session: session, request: request) } - func removeWireguardKey(accountToken: String, publicKey: Data) -> AnyPublisher<Bool, MullvadRpc.Error> { + func checkWireguardKey(request: MullvadRpc.Request<Bool>? = nil) -> MullvadRpc.Operation<Bool> { + return MullvadRpc.Operation(request: request) + } + + func removeWireguardKey(accountToken: String, publicKey: Data) -> MullvadRpc.Request<Bool> { let request = JsonRpcRequest(method: "remove_wg_key", params: [ AnyEncodable(accountToken), AnyEncodable(publicKey) ]) - return makeDataTaskPublisher(request: request) + return MullvadRpc.Request(session: session, request: request) } - func sendAppStoreReceipt(accountToken: String, receiptData: Data) -> AnyPublisher<SendAppStoreReceiptResponse, MullvadRpc.Error> { + func sendAppStoreReceipt(accountToken: String, receiptData: Data) -> MullvadRpc.Request<SendAppStoreReceiptResponse> { let request = JsonRpcRequest(method: "apple_payment", params: [ AnyEncodable(accountToken), AnyEncodable(receiptData) ]) - return makeDataTaskPublisher(request: request) - } - - private func makeDataTaskPublisher<T: Decodable>(request: JsonRpcRequest) -> AnyPublisher<T, MullvadRpc.Error> { - return Just(request) - .encode(encoder: Self.makeJSONEncoder()) - .mapError { MullvadRpc.Error.encoding($0) } - .map { Self.makeURLRequest(httpBody: $0) } - .flatMap { - self.session.dataTaskPublisher(for: $0) - .mapError { MullvadRpc.Error.network($0) } - .flatMap { (data, httpResponse) in - Just(data) - .decode(type: JsonRpcResponse<T, ResponseCode>.self, decoder: Self.makeJSONDecoder()) - .mapError { MullvadRpc.Error.decoding($0) } - .flatMap { (serverResponse) in - // unwrap JsonRpcResponse.result - serverResponse.result - .mapError { MullvadRpc.Error.server($0) } - .publisher - } - } - }.eraseToAnyPublisher() - } - - 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 - return encoder - } - - private static func makeJSONDecoder() -> JSONDecoder { - let decoder = JSONDecoder() - decoder.keyDecodingStrategy = .convertFromSnakeCase - decoder.dateDecodingStrategy = .iso8601 - decoder.dataDecodingStrategy = .base64 - return decoder + return MullvadRpc.Request(session: session, request: request) } } @@ -252,3 +228,120 @@ extension JsonRpcResponseError: LocalizedError } } } + + +extension MullvadRpc { + + class Request<Response: Decodable> { + typealias RequestCompletionHandler = (Result<Response, MullvadRpc.Error>) -> Void + + private let session: URLSession + private let request: JsonRpcRequest + + private let lock = NSLock() + private var urlSessionTask: URLSessionTask? + + fileprivate init(session: URLSession, request: JsonRpcRequest) { + self.session = session + self.request = request + } + + func start(completionHandler: @escaping RequestCompletionHandler) { + lock.withCriticalBlock { + assert(self.urlSessionTask == nil) + + switch makeURLRequest() { + case .success(let urlRequest): + let task = session.dataTask(with: urlRequest) { (responseData, urlResponse, error) in + switch (responseData, error) { + case (.some(let data), .none): + completionHandler(Self.decodeResponse(data)) + + case (.none, .some(let urlError as URLError)): + completionHandler(.failure(.network(urlError))) + + default: + fatalError() + } + } + self.urlSessionTask = task + task.resume() + + case .failure(let error): + completionHandler(.failure(error)) + } + } + } + + func cancel() { + lock.withCriticalBlock { + self.urlSessionTask?.cancel() + } + } + + func operation() -> MullvadRpc.Operation<Response> { + return MullvadRpc.Operation(request: self) + } + + private func makeURLRequest() -> Result<URLRequest, MullvadRpc.Error> { + do { + let data = try MullvadRpc.makeJSONEncoder().encode(request) + + return .success(Self.makeURLRequest(httpBody: data)) + } catch { + return .failure(.encoding(error)) + } + } + + private static func decodeResponse(_ responseData: Data) -> Result<Response, MullvadRpc.Error> { + do { + let serverResponse = try MullvadRpc.makeJSONDecoder() + .decode(JsonRpcResponse<Response, MullvadRpc.ResponseCode>.self, from: responseData) + + // unwrap JsonRpcResponse.result + return serverResponse.result + .mapError { .server($0) } + } catch { + return .failure(.decoding(error)) + } + } + + 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 + } + } + + class Operation<Response>: AsyncOperation, InputOperation, OutputOperation where Response: Decodable { + typealias Input = Request<Response> + typealias Output = Result<Response, MullvadRpc.Error> + + init(request: Input? = nil) { + super.init() + self.input = request + } + + override func main() { + guard let request = self.input else { + self.finish() + return + } + + request.start { [weak self] (result) in + self?.finish(with: result) + } + } + + override func operationDidCancel() { + input?.cancel() + } + } +} |
