1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
|
//
// MullvadAPI.swift
// MullvadVPN
//
// Created by pronebird on 02/05/2019.
// Copyright © 2019 Amagicom AB. All rights reserved.
//
import Foundation
import Network
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
init(session: URLSession = URLSession.shared) {
self.session = session
}
func getRelayList() -> AnyPublisher<JsonRpcResponse<RelayList>, MullvadAPIError> {
let request = JsonRpcRequest(method: "relay_list_v3", params: [])
return MullvadAPI.makeDataTaskPublisher(request: request)
}
func getAccountExpiry(accountToken: String) -> AnyPublisher<JsonRpcResponse<Date>, MullvadAPIError> {
let request = JsonRpcRequest(method: "get_expiry", params: [AnyEncodable(accountToken)])
return MullvadAPI.makeDataTaskPublisher(request: request)
}
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()
}
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)
}
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 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
}
}
|