summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAndrej Mihajlov <and@mullvad.net>2022-05-17 15:24:07 +0200
committerAndrej Mihajlov <and@mullvad.net>2022-05-17 15:24:07 +0200
commit92edd7fdfe5ab31bd6ba02614b9d520dfcb82ea2 (patch)
tree8629c5d24cbd619ae797156265b592acc38b5fce
parente59802548263b33bb428e5172cdb2b7833caea5f (diff)
parentb738918b26e5d62a910a984ce9154547e9710764 (diff)
downloadmullvadvpn-92edd7fdfe5ab31bd6ba02614b9d520dfcb82ea2.tar.xz
mullvadvpn-92edd7fdfe5ab31bd6ba02614b9d520dfcb82ea2.zip
Merge branch 'add-new-rest-methods'
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj4
-rw-r--r--ios/MullvadVPN.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved2
-rw-r--r--ios/MullvadVPN/AppStorePaymentManager/SendAppStoreReceiptOperation.swift2
-rw-r--r--ios/MullvadVPN/DisplayChainedError.swift58
-rw-r--r--ios/MullvadVPN/LoginViewController.swift12
-rw-r--r--ios/MullvadVPN/REST/HTTP.swift16
-rw-r--r--ios/MullvadVPN/REST/RESTAPIProxy.swift231
-rw-r--r--ios/MullvadVPN/REST/RESTAccountsProxy.swift70
-rw-r--r--ios/MullvadVPN/REST/RESTAuthenticationProxy.swift24
-rw-r--r--ios/MullvadVPN/REST/RESTAuthorization.swift42
-rw-r--r--ios/MullvadVPN/REST/RESTCoding.swift31
-rw-r--r--ios/MullvadVPN/REST/RESTDevicesProxy.swift290
-rw-r--r--ios/MullvadVPN/REST/RESTError.swift136
-rw-r--r--ios/MullvadVPN/REST/RESTNetworkOperation.swift112
-rw-r--r--ios/MullvadVPN/REST/RESTProxy.swift4
-rw-r--r--ios/MullvadVPN/REST/RESTRequestFactory.swift149
-rw-r--r--ios/MullvadVPN/REST/RESTRequestHandler.swift52
-rw-r--r--ios/MullvadVPN/REST/RESTResponseDecoder.swift46
-rw-r--r--ios/MullvadVPN/REST/RESTResponseHandler.swift35
-rw-r--r--ios/MullvadVPN/TunnelManager/SetAccountOperation.swift3
-rw-r--r--ios/MullvadVPN/TunnelManager/TunnelManager.swift3
-rw-r--r--ios/MullvadVPN/WireguardKeysViewController.swift3
22 files changed, 830 insertions, 495 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj
index 21cc7f746b..4fdfac7208 100644
--- a/ios/MullvadVPN.xcodeproj/project.pbxproj
+++ b/ios/MullvadVPN.xcodeproj/project.pbxproj
@@ -104,7 +104,6 @@
5850368C25A49E2200A43E93 /* PrivateKeyWithMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C6B35322BB87C4003C19AD /* PrivateKeyWithMetadata.swift */; };
5850368D25A49E2200A43E93 /* PrivateKeyWithMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C6B35322BB87C4003C19AD /* PrivateKeyWithMetadata.swift */; };
58554F73280AFA5A00013055 /* RESTAuthenticationProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58554F72280AFA5A00013055 /* RESTAuthenticationProxy.swift */; };
- 58554F75280AFAE900013055 /* RESTResponseDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58554F74280AFAE900013055 /* RESTResponseDecoder.swift */; };
58554F77280AFD5C00013055 /* RESTTaskIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58554F76280AFD5C00013055 /* RESTTaskIdentifier.swift */; };
58554F79280B037400013055 /* RESTAccessTokenManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58554F78280B037400013055 /* RESTAccessTokenManager.swift */; };
58554F7B280B125F00013055 /* RESTAccountsProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58554F7A280B125F00013055 /* RESTAccountsProxy.swift */; };
@@ -424,7 +423,6 @@
584EBDBC2747C98F00A0C9FD /* NSAttributedString+Markdown.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSAttributedString+Markdown.swift"; sourceTree = "<group>"; };
5850366725A47AC700A43E93 /* IPAddressRange+Codable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "IPAddressRange+Codable.swift"; sourceTree = "<group>"; };
58554F72280AFA5A00013055 /* RESTAuthenticationProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RESTAuthenticationProxy.swift; sourceTree = "<group>"; };
- 58554F74280AFAE900013055 /* RESTResponseDecoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RESTResponseDecoder.swift; sourceTree = "<group>"; };
58554F76280AFD5C00013055 /* RESTTaskIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RESTTaskIdentifier.swift; sourceTree = "<group>"; };
58554F78280B037400013055 /* RESTAccessTokenManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RESTAccessTokenManager.swift; sourceTree = "<group>"; };
58554F7A280B125F00013055 /* RESTAccountsProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RESTAccountsProxy.swift; sourceTree = "<group>"; };
@@ -779,7 +777,6 @@
58F97A1A280EEBC00050C2FC /* RESTProxyFactory.swift */,
58B5A894280AACC4009FDE99 /* RESTRequestFactory.swift */,
58F97A1D280FDE230050C2FC /* RESTRequestHandler.swift */,
- 58554F74280AFAE900013055 /* RESTResponseDecoder.swift */,
588BCF272816D664009ADCEC /* RESTResponseHandler.swift */,
58095C582762155700890776 /* RESTRetryStrategy.swift */,
58554F76280AFD5C00013055 /* RESTTaskIdentifier.swift */,
@@ -1496,7 +1493,6 @@
58FD5BF024238EB300112C88 /* SKProduct+Formatting.swift in Sources */,
58B43C1925F77DB60002C8C3 /* ConnectMainContentView.swift in Sources */,
58561C99239A5D1500BD6B5E /* IPEndpoint.swift in Sources */,
- 58554F75280AFAE900013055 /* RESTResponseDecoder.swift in Sources */,
58F97A1E280FDE230050C2FC /* RESTRequestHandler.swift in Sources */,
58FD5BF22424F7D700112C88 /* UserInterfaceInteractionRestriction.swift in Sources */,
5811DE50239014550011EB53 /* NEVPNStatus+Debug.swift in Sources */,
diff --git a/ios/MullvadVPN.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ios/MullvadVPN.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
index 5493113eef..83c0aec460 100644
--- a/ios/MullvadVPN.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
+++ b/ios/MullvadVPN.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -15,7 +15,7 @@
"repositoryURL": "https://github.com/mullvad/wireguard-apple.git",
"state": {
"branch": "mullvad-master",
- "revision": "736a4bb5baba4e2c70686f59416d50c3b06e8424",
+ "revision": "eeb980058f5bff593868e34b718b4519a3236dcc",
"version": null
}
}
diff --git a/ios/MullvadVPN/AppStorePaymentManager/SendAppStoreReceiptOperation.swift b/ios/MullvadVPN/AppStorePaymentManager/SendAppStoreReceiptOperation.swift
index 3bcfa7c4f3..befd7da698 100644
--- a/ios/MullvadVPN/AppStorePaymentManager/SendAppStoreReceiptOperation.swift
+++ b/ios/MullvadVPN/AppStorePaymentManager/SendAppStoreReceiptOperation.swift
@@ -65,7 +65,7 @@ class SendAppStoreReceiptOperation: ResultOperation<REST.CreateApplePaymentRespo
private func sendReceipt(_ receiptData: Data) {
submitReceiptTask = apiProxy.createApplePayment(
- token: self.accountToken,
+ accountNumber: self.accountToken,
receiptString: receiptData,
retryStrategy: .noRetry) { result in
switch result {
diff --git a/ios/MullvadVPN/DisplayChainedError.swift b/ios/MullvadVPN/DisplayChainedError.swift
index 5e3d154f28..aa7ec26b2d 100644
--- a/ios/MullvadVPN/DisplayChainedError.swift
+++ b/ios/MullvadVPN/DisplayChainedError.swift
@@ -22,44 +22,34 @@ extension REST.Error: DisplayChainedError {
"NETWORK_ERROR",
tableName: "REST",
value: "Network error: %@",
- comment: "Network error. Use %@ placeholder to place localized failure description."
+ comment: ""
),
urlError.localizedDescription
)
- case .server(let serverError):
- if let knownErrorDescription = serverError.errorDescription {
- return knownErrorDescription
- } else {
- return String(
- format: NSLocalizedString(
- "SERVER_ERROR",
- tableName: "REST",
- value: "Server error: %@",
- comment: "Server error. Use %@ placeholder to place localized failure description."
- ),
- serverError.error ?? "(empty)"
- )
- }
- case .encodePayload:
+ case .unhandledResponse(let statusCode, let serverResponse):
+ return String(
+ format: NSLocalizedString(
+ "SERVER_ERROR",
+ tableName: "REST",
+ value: "Unexpected server response: %1$@ (HTTP status: %2$d)",
+ comment: ""
+ ),
+ serverResponse?.code.rawValue ?? "(no code)",
+ statusCode
+ )
+ case .createURLRequest:
return NSLocalizedString(
"SERVER_REQUEST_ENCODING_ERROR",
tableName: "REST",
- value: "Server request encoding error",
- comment: "Failure to encode the server request."
+ value: "Failure to create URL request",
+ comment: ""
)
- case .decodeSuccessResponse:
+ case .decodeResponse:
return NSLocalizedString(
"SERVER_SUCCESS_RESPONSE_DECODING_ERROR",
tableName: "REST",
- value: "Server success response decoding error",
- comment: "Failure to decode the server success response."
- )
- case .decodeErrorResponse:
- return NSLocalizedString(
- "SERVER_FAILURE_RESPONSE_DECODING_ERROR",
- tableName: "REST",
- value: "Server error response decoding error",
- comment: "Failure to decode the server failure response."
+ value: "Server response decoding error",
+ comment: ""
)
}
}
@@ -194,7 +184,9 @@ extension TunnelManager.Error: DisplayChainedError {
reason
)
- if case .server(.keyLimitReached) = restError {
+ if case .unhandledResponse(_, let serverErrorResponse) = restError,
+ serverErrorResponse?.code == .keyLimitReached
+ {
// TODO: maybe use `restError.recoverySuggestion` instead?
message.append("\n\n")
message.append(NSLocalizedString(
@@ -219,7 +211,9 @@ extension TunnelManager.Error: DisplayChainedError {
reason
)
- if case .server(.keyLimitReached) = restError {
+ if case .unhandledResponse(_, let serverErrorResponse) = restError,
+ serverErrorResponse?.code == .keyLimitReached
+ {
// TODO: maybe use `restError.recoverySuggestion` instead?
message.append("\n\n")
message.append(NSLocalizedString(
@@ -346,7 +340,9 @@ extension AppStorePaymentManager.Error: DisplayChainedError {
case .validateAccount(let restError):
let reason = restError.errorChainDescription ?? ""
- if case .server(.invalidAccount) = restError {
+ if case .unhandledResponse(_, let serverErrorResponse) = restError,
+ serverErrorResponse?.code == .invalidAccount
+ {
return String(
format: NSLocalizedString(
"INVALID_ACCOUNT_ERROR",
diff --git a/ios/MullvadVPN/LoginViewController.swift b/ios/MullvadVPN/LoginViewController.swift
index fa63d40ed7..f345798941 100644
--- a/ios/MullvadVPN/LoginViewController.swift
+++ b/ios/MullvadVPN/LoginViewController.swift
@@ -364,20 +364,14 @@ private extension LoginState {
urlError.localizedDescription
)
- case .server(let serverError):
- var message = serverError.errorDescription ?? NSLocalizedString(
+ case .unhandledResponse(_, let serverError):
+ return serverError?.detail ?? NSLocalizedString(
"SUBHEAD_TITLE_UNKNOWN_SERVER_ERROR",
tableName: "Login",
comment: "Subhead displayed in the event of unknown server error."
)
- if let recoverySuggestion = serverError.recoverySuggestion {
- message.append("\n\(recoverySuggestion)")
- }
-
- return message
-
- case .encodePayload, .decodeErrorResponse, .decodeSuccessResponse:
+ case .createURLRequest, .decodeResponse:
return localizedUnknownInternalError
}
} else {
diff --git a/ios/MullvadVPN/REST/HTTP.swift b/ios/MullvadVPN/REST/HTTP.swift
index 595809ee0e..82178dc2f5 100644
--- a/ios/MullvadVPN/REST/HTTP.swift
+++ b/ios/MullvadVPN/REST/HTTP.swift
@@ -13,6 +13,7 @@ struct HTTPMethod: RawRepresentable {
static let get = HTTPMethod(rawValue: "GET")
static let post = HTTPMethod(rawValue: "POST")
static let delete = HTTPMethod(rawValue: "DELETE")
+ static let put = HTTPMethod(rawValue: "PUT")
let rawValue: String
init(rawValue: String) {
@@ -20,12 +21,23 @@ struct HTTPMethod: RawRepresentable {
}
}
-enum HTTPStatus {
- static let notModified = 304
+struct HTTPStatus: RawRepresentable, Equatable {
+ static let notModified = HTTPStatus(rawValue: 304)
+ static let badRequest = HTTPStatus(rawValue: 400)
+ static let notFound = HTTPStatus(rawValue: 404)
static func isSuccess(_ code: Int) -> Bool {
return (200..<300).contains(code)
}
+
+ let rawValue: Int
+ init(rawValue: Int) {
+ self.rawValue = rawValue
+ }
+
+ var isSuccess: Bool {
+ return Self.isSuccess(rawValue)
+ }
}
/// HTTP headers
diff --git a/ios/MullvadVPN/REST/RESTAPIProxy.swift b/ios/MullvadVPN/REST/RESTAPIProxy.swift
index 15cc807c16..1271422be1 100644
--- a/ios/MullvadVPN/REST/RESTAPIProxy.swift
+++ b/ios/MullvadVPN/REST/RESTAPIProxy.swift
@@ -21,9 +21,7 @@ extension REST {
pathPrefix: "/app/v1",
bodyEncoder: Coding.makeJSONEncoder()
),
- responseDecoder: ResponseDecoder(
- decoder: Coding.makeJSONDecoder()
- )
+ responseDecoder: Coding.makeJSONDecoder()
)
}
@@ -33,13 +31,11 @@ extension REST {
) -> Cancellable
{
let requestHandler = AnyRequestHandler { endpoint in
- let request = self.requestFactory.createURLRequest(
+ return try self.requestFactory.createRequest(
endpoint: endpoint,
method: .post,
- path: "accounts"
+ pathTemplate: "accounts"
)
-
- return .success(request)
}
let responseHandler = REST.defaultResponseHandler(
@@ -62,13 +58,11 @@ extension REST {
) -> Cancellable
{
let requestHandler = AnyRequestHandler { endpoint in
- let request = self.requestFactory.createURLRequest(
+ return try self.requestFactory.createRequest(
endpoint: endpoint,
method: .get,
- path: "api-addrs"
+ pathTemplate: "api-addrs"
)
-
- return .success(request)
}
let responseHandler = REST.defaultResponseHandler(
@@ -92,30 +86,44 @@ extension REST {
) -> Cancellable
{
let requestHandler = AnyRequestHandler { endpoint in
- var requestBuilder = self.requestFactory.createURLRequestBuilder(
+ var requestBuilder = try self.requestFactory.createRequestBuilder(
endpoint: endpoint,
method: .get,
- path: "relays"
+ pathTemplate: "relays"
)
if let etag = etag {
requestBuilder.setETagHeader(etag: etag)
}
- return .success(requestBuilder.getURLRequest())
+ return requestBuilder.getRequest()
}
- let responseHandler = AnyResponseHandler { response, data -> Result<ServerRelaysCacheResponse, REST.Error> in
- if HTTPStatus.isSuccess(response.statusCode) {
- return self.responseDecoder.decodeSuccessResponse(ServerRelaysResponse.self, from: data)
- .map { serverRelays in
- let newEtag = response.value(forCaseInsensitiveHTTPHeaderField: HTTPHeader.etag)
- return .newContent(newEtag, serverRelays)
- }
- } else if response.statusCode == HTTPStatus.notModified && etag != nil {
+ let responseHandler = AnyResponseHandler { response, data -> ResponseHandlerResult<ServerRelaysCacheResponse> in
+ let httpStatus = HTTPStatus(rawValue: response.statusCode)
+
+ switch httpStatus {
+ case let httpStatus where httpStatus.isSuccess:
+ return .decoding {
+ let serverRelays = try self.responseDecoder.decode(
+ ServerRelaysResponse.self,
+ from: data
+ )
+ let newEtag = response.value(forCaseInsensitiveHTTPHeaderField: HTTPHeader.etag)
+
+ return .newContent(newEtag, serverRelays)
+ }
+
+ case .notModified where etag != nil:
return .success(.notModified)
- } else {
- return self.responseDecoder.decodeErrorResponseAndMapToServerError(from: data)
+
+ default:
+ return .unhandledResponse(
+ try? self.responseDecoder.decode(
+ ServerErrorResponse.self,
+ from: data
+ )
+ )
}
}
@@ -135,15 +143,15 @@ extension REST {
) -> Cancellable
{
let requestHandler = AnyRequestHandler { endpoint in
- var requestBuilder = self.requestFactory
- .createURLRequestBuilder(
+ var requestBuilder = try self.requestFactory
+ .createRequestBuilder(
endpoint: endpoint,
method: .get,
- path: "me"
+ pathTemplate: "me"
)
requestBuilder.setAuthorization(.accountNumber(accountNumber))
- return .success(requestBuilder.getURLRequest())
+ return requestBuilder.getRequest()
}
let responseHandler = REST.defaultResponseHandler(
@@ -168,19 +176,22 @@ extension REST {
) -> Cancellable
{
let requestHandler = AnyRequestHandler { endpoint in
- let urlEncodedPublicKey = publicKey.base64Key
- .addingPercentEncoding(withAllowedCharacters: .alphanumerics)!
- let path = "wireguard-keys/".appending(urlEncodedPublicKey)
+ var path: URLPathTemplate = "wireguard-keys/{pubkey}"
+ try path.addPercentEncodedReplacement(
+ name: "pubkey",
+ value: publicKey.base64Key,
+ allowedCharacters: .alphanumerics
+ )
- var requestBuilder = self.requestFactory
- .createURLRequestBuilder(
+ var requestBuilder = try self.requestFactory
+ .createRequestBuilder(
endpoint: endpoint,
method: .get,
- path: path
+ pathTemplate: path
)
requestBuilder.setAuthorization(.accountNumber(accountNumber))
- return .success(requestBuilder.getURLRequest())
+ return requestBuilder.getRequest()
}
let responseHandler = REST.defaultResponseHandler(
@@ -205,25 +216,20 @@ extension REST {
) -> Cancellable
{
let requestHandler = AnyRequestHandler { endpoint in
- var requestBuilder = self.requestFactory.createURLRequestBuilder(
+ var requestBuilder = try self.requestFactory.createRequestBuilder(
endpoint: endpoint,
method: .post,
- path: "wireguard-keys"
+ pathTemplate: "wireguard-keys"
)
requestBuilder.setAuthorization(.accountNumber(accountNumber))
- return Result {
- let body = PushWireguardKeyRequest(
- pubkey: publicKey.rawValue
- )
- try requestBuilder.setHTTPBody(value: body)
- }
- .mapError { error in
- return .encodePayload(error)
- }
- .map { _ in
- return requestBuilder.getURLRequest()
- }
+ let body = PushWireguardKeyRequest(
+ pubkey: publicKey.rawValue
+ )
+
+ try requestBuilder.setHTTPBody(value: body)
+
+ return requestBuilder.getRequest()
}
let responseHandler = REST.defaultResponseHandler(
@@ -249,26 +255,21 @@ extension REST {
) -> Cancellable
{
let requestHandler = AnyRequestHandler { endpoint in
- var requestBuilder = self.requestFactory.createURLRequestBuilder(
+ var requestBuilder = try self.requestFactory.createRequestBuilder(
endpoint: endpoint,
method: .post,
- path: "replace-wireguard-key"
+ pathTemplate: "replace-wireguard-key"
)
requestBuilder.setAuthorization(.accountNumber(accountNumber))
- return Result {
- let body = ReplaceWireguardKeyRequest(
- old: oldPublicKey.rawValue,
- new: newPublicKey.rawValue
- )
- try requestBuilder.setHTTPBody(value: body)
- }
- .mapError { error in
- return .encodePayload(error)
- }
- .map { _ in
- return requestBuilder.getURLRequest()
- }
+ let body = ReplaceWireguardKeyRequest(
+ old: oldPublicKey.rawValue,
+ new: newPublicKey.rawValue
+ )
+
+ try requestBuilder.setHTTPBody(value: body)
+
+ return requestBuilder.getRequest()
}
let responseHandler = REST.defaultResponseHandler(
@@ -293,26 +294,35 @@ extension REST {
) -> Cancellable
{
let requestHandler = AnyRequestHandler { endpoint in
- let urlEncodedPublicKey = publicKey.base64Key
- .addingPercentEncoding(withAllowedCharacters: .alphanumerics)!
+ var path: URLPathTemplate = "wireguard-keys/{pubkey}"
- let path = "wireguard-keys/".appending(urlEncodedPublicKey)
- var requestBuilder = self.requestFactory
- .createURLRequestBuilder(
+ try path.addPercentEncodedReplacement(
+ name: "pubkey",
+ value: publicKey.base64Key,
+ allowedCharacters: .alphanumerics
+ )
+
+ var requestBuilder = try self.requestFactory
+ .createRequestBuilder(
endpoint: endpoint,
method: .delete,
- path: path
+ pathTemplate: path
)
requestBuilder.setAuthorization(.accountNumber(accountNumber))
- return .success(requestBuilder.getURLRequest())
+ return requestBuilder.getRequest()
}
- let responseHandler = AnyResponseHandler { response, data -> Result<Void, REST.Error> in
+ let responseHandler = AnyResponseHandler { response, data -> ResponseHandlerResult<Void> in
if HTTPStatus.isSuccess(response.statusCode) {
return .success(())
} else {
- return self.responseDecoder.decodeErrorResponseAndMapToServerError(from: data)
+ return .unhandledResponse(
+ try? self.responseDecoder.decode(
+ ServerErrorResponse.self,
+ from: data
+ )
+ )
}
}
@@ -333,40 +343,42 @@ extension REST {
) -> Cancellable
{
let requestHandler = AnyRequestHandler { endpoint in
- var requestBuilder = self.requestFactory
- .createURLRequestBuilder(
+ var requestBuilder = try self.requestFactory
+ .createRequestBuilder(
endpoint: endpoint,
method: .post,
- path: "create-apple-payment"
+ pathTemplate: "create-apple-payment"
)
requestBuilder.setAuthorization(.accountNumber(accountNumber))
- return Result {
- let body = CreateApplePaymentRequest(
- receiptString: receiptString
- )
- try requestBuilder.setHTTPBody(value: body)
- }
- .mapError { error in
- return .encodePayload(error)
- }
- .map { _ in
- return requestBuilder.getURLRequest()
- }
+ let body = CreateApplePaymentRequest(
+ receiptString: receiptString
+ )
+ try requestBuilder.setHTTPBody(value: body)
+
+ return requestBuilder.getRequest()
}
- let responseHandler = AnyResponseHandler { response, data -> Result<CreateApplePaymentResponse, REST.Error> in
+ let responseHandler = AnyResponseHandler { response, data -> ResponseHandlerResult<CreateApplePaymentResponse> in
if HTTPStatus.isSuccess(response.statusCode) {
- return self.responseDecoder.decodeSuccessResponse(CreateApplePaymentRawResponse.self, from: data)
- .map { (response) in
- if response.timeAdded > 0 {
- return .timeAdded(response.timeAdded, response.newExpiry)
- } else {
- return .noTimeAdded(response.newExpiry)
- }
+ return .decoding {
+ let serverResponse = try self.responseDecoder.decode(
+ CreateApplePaymentRawResponse.self,
+ from: data
+ )
+ if serverResponse.timeAdded > 0 {
+ return .timeAdded(serverResponse.timeAdded, serverResponse.newExpiry)
+ } else {
+ return .noTimeAdded(serverResponse.newExpiry)
}
+ }
} else {
- return self.responseDecoder.decodeErrorResponseAndMapToServerError(from: data)
+ return .unhandledResponse(
+ try? self.responseDecoder.decode(
+ ServerErrorResponse.self,
+ from: data
+ )
+ )
}
}
@@ -386,28 +398,27 @@ extension REST {
) -> Cancellable
{
let requestHandler = AnyRequestHandler { endpoint in
- var requestBuilder = self.requestFactory.createURLRequestBuilder(
+ var requestBuilder = try self.requestFactory.createRequestBuilder(
endpoint: endpoint,
method: .post,
- path: "problem-report"
+ pathTemplate: "problem-report"
)
- return Result {
- try requestBuilder.setHTTPBody(value: body)
- }
- .mapError { error in
- return .encodePayload(error)
- }
- .map { _ in
- return requestBuilder.getURLRequest()
- }
+ try requestBuilder.setHTTPBody(value: body)
+
+ return requestBuilder.getRequest()
}
- let responseHandler = AnyResponseHandler { response, data -> Result<Void, REST.Error> in
+ let responseHandler = AnyResponseHandler { response, data -> ResponseHandlerResult<Void> in
if HTTPStatus.isSuccess(response.statusCode) {
return .success(())
} else {
- return self.responseDecoder.decodeErrorResponseAndMapToServerError(from: data)
+ return .unhandledResponse(
+ try? self.responseDecoder.decode(
+ ServerErrorResponse.self,
+ from: data
+ )
+ )
}
}
diff --git a/ios/MullvadVPN/REST/RESTAccountsProxy.swift b/ios/MullvadVPN/REST/RESTAccountsProxy.swift
index 2bc05e088c..e9c44f0f63 100644
--- a/ios/MullvadVPN/REST/RESTAccountsProxy.swift
+++ b/ios/MullvadVPN/REST/RESTAccountsProxy.swift
@@ -15,48 +15,65 @@ extension REST {
name: "AccountsProxy",
configuration: configuration,
requestFactory: RequestFactory.withDefaultAPICredentials(
- pathPrefix: "/accounts/v1-beta1",
+ pathPrefix: "/accounts/v1",
bodyEncoder: Coding.makeJSONEncoder()
),
- responseDecoder: ResponseDecoder(
- decoder: Coding.makeJSONDecoder()
+ responseDecoder: Coding.makeJSONDecoder()
+ )
+ }
+
+ func createAccount(
+ retryStrategy: REST.RetryStrategy,
+ completion: @escaping CompletionHandler<NewAccountData>
+ ) -> Cancellable {
+ let requestHandler = AnyRequestHandler { endpoint in
+ return try self.requestFactory.createRequest(
+ endpoint: endpoint,
+ method: .post,
+ pathTemplate: "accounts"
)
+ }
+
+ let responseHandler = REST.defaultResponseHandler(
+ decoding: NewAccountData.self,
+ with: responseDecoder
+ )
+
+ return addOperation(
+ name: "create-account",
+ retryStrategy: retryStrategy,
+ requestHandler: requestHandler,
+ responseHandler: responseHandler,
+ completionHandler: completion
)
}
- func getMyAccount(
+ func getAccountData(
accountNumber: String,
retryStrategy: REST.RetryStrategy,
- completion: @escaping CompletionHandler<BetaAccountResponse>
+ completion: @escaping CompletionHandler<AccountData>
) -> Cancellable
{
let requestHandler = AnyRequestHandler(
createURLRequest: { endpoint, authorization in
- var requestBuilder = self.requestFactory.createURLRequestBuilder(
+ var requestBuilder = try self.requestFactory.createRequestBuilder(
endpoint: endpoint,
method: .get,
- path: "/accounts/me"
+ pathTemplate: "accounts/me"
)
requestBuilder.setAuthorization(authorization)
- return .success(requestBuilder.getURLRequest())
+ return requestBuilder.getRequest()
},
- requestAuthorization: { completion in
- return self.configuration.accessTokenManager
- .getAccessToken(
- accountNumber: accountNumber,
- retryStrategy: retryStrategy
- ) { operationCompletion in
- completion(operationCompletion.map { tokenData in
- return .accessToken(tokenData.accessToken)
- })
- }
- }
+ authorizationProvider: createAuthorizationProvider(
+ accountNumber: accountNumber,
+ retryStrategy: .default
+ )
)
let responseHandler = REST.defaultResponseHandler(
- decoding: BetaAccountResponse.self,
+ decoding: AccountData.self,
with: responseDecoder
)
@@ -70,13 +87,22 @@ extension REST {
}
}
- struct BetaAccountResponse: Decodable {
+ struct AccountData: Decodable {
+ let id: String
+ let expiry: Date
+ let maxPorts: Int
+ let canAddPorts: Bool
+ let maxDevices: Int
+ let canAddDevices: Bool
+ }
+
+ struct NewAccountData: Decodable {
let id: String
- let number: String
let expiry: Date
let maxPorts: Int
let canAddPorts: Bool
let maxDevices: Int
let canAddDevices: Bool
+ let number: String
}
}
diff --git a/ios/MullvadVPN/REST/RESTAuthenticationProxy.swift b/ios/MullvadVPN/REST/RESTAuthenticationProxy.swift
index c4ab8e9d04..a9843ab750 100644
--- a/ios/MullvadVPN/REST/RESTAuthenticationProxy.swift
+++ b/ios/MullvadVPN/REST/RESTAuthenticationProxy.swift
@@ -15,12 +15,10 @@ extension REST {
name: "AuthenticationProxy",
configuration: configuration,
requestFactory: RequestFactory.withDefaultAPICredentials(
- pathPrefix: "/auth/v1-beta1",
+ pathPrefix: "/auth/v1",
bodyEncoder: Coding.makeJSONEncoder()
),
- responseDecoder: ResponseDecoder(
- decoder: Coding.makeJSONDecoder()
- )
+ responseDecoder: Coding.makeJSONDecoder()
)
}
@@ -31,23 +29,17 @@ extension REST {
) -> Cancellable
{
let requestHandler = AnyRequestHandler { endpoint in
- var requestBuilder = self.requestFactory.createURLRequestBuilder(
+ var requestBuilder = try self.requestFactory.createRequestBuilder(
endpoint: endpoint,
method: .post,
- path: "/token"
+ pathTemplate: "token"
)
- return Result {
- let request = AccessTokenRequest(accountNumber: accountNumber)
+ let request = AccessTokenRequest(accountNumber: accountNumber)
+
+ try requestBuilder.setHTTPBody(value: request)
- try requestBuilder.setHTTPBody(value: request)
- }
- .mapError { error in
- return .encodePayload(error)
- }
- .map { _ in
- return requestBuilder.getURLRequest()
- }
+ return requestBuilder.getRequest()
}
let responseHandler = REST.defaultResponseHandler(
diff --git a/ios/MullvadVPN/REST/RESTAuthorization.swift b/ios/MullvadVPN/REST/RESTAuthorization.swift
index b9a701b1ac..864cdb83a6 100644
--- a/ios/MullvadVPN/REST/RESTAuthorization.swift
+++ b/ios/MullvadVPN/REST/RESTAuthorization.swift
@@ -8,9 +8,51 @@
import Foundation
+protocol RESTAuthorizationProvider {
+ typealias Completion = OperationCompletion<REST.Authorization, REST.Error>
+
+ func getAuthorization(completion: @escaping (Completion) -> Void) -> Cancellable
+}
+
extension REST {
enum Authorization {
case accountNumber(String)
case accessToken(String)
}
+
+ struct AccessTokenProvider: RESTAuthorizationProvider {
+ private let accessTokenManager: AccessTokenManager
+ private let accountNumber: String
+ private let retryStrategy: REST.RetryStrategy
+
+ init(accessTokenManager: AccessTokenManager, accountNumber: String, retryStrategy: REST.RetryStrategy) {
+ self.accessTokenManager = accessTokenManager
+ self.accountNumber = accountNumber
+ self.retryStrategy = retryStrategy
+ }
+
+ func getAuthorization(completion: @escaping (Completion) -> Void) -> Cancellable {
+ return accessTokenManager.getAccessToken(
+ accountNumber: accountNumber,
+ retryStrategy: retryStrategy
+ ) { operationCompletion in
+ completion(operationCompletion.map { tokenData in
+ return .accessToken(tokenData.accessToken)
+ })
+ }
+ }
+ }
+}
+
+extension REST.Proxy where ConfigurationType == REST.AuthProxyConfiguration {
+ func createAuthorizationProvider(
+ accountNumber: String,
+ retryStrategy: REST.RetryStrategy
+ ) -> RESTAuthorizationProvider {
+ return REST.AccessTokenProvider(
+ accessTokenManager: configuration.accessTokenManager,
+ accountNumber: accountNumber,
+ retryStrategy: retryStrategy
+ )
+ }
}
diff --git a/ios/MullvadVPN/REST/RESTCoding.swift b/ios/MullvadVPN/REST/RESTCoding.swift
index 8a5356e3bc..b8c917e6b4 100644
--- a/ios/MullvadVPN/REST/RESTCoding.swift
+++ b/ios/MullvadVPN/REST/RESTCoding.swift
@@ -27,36 +27,7 @@ extension REST.Coding {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
decoder.dataDecodingStrategy = .base64
-
- let iso8601Formatter = ISO8601DateFormatter()
-
- // Setup additional formatter to account for fractional seconds returned
- // by some of the API calls.
- lazy var iso8601WithSubSecondsFormatter: ISO8601DateFormatter = {
- let formatter = ISO8601DateFormatter()
- formatter.formatOptions.insert(.withFractionalSeconds)
- return formatter
- }()
-
- decoder.dateDecodingStrategy = .custom({ decoder in
- let container = try decoder.singleValueContainer()
- let value = try container.decode(String.self)
-
- let date = iso8601Formatter.date(from: value) ??
- iso8601WithSubSecondsFormatter.date(from: value)
-
- switch date {
- case .some(let parsedDate):
- return parsedDate
-
- case .none:
- throw DecodingError.dataCorruptedError(
- in: container,
- debugDescription: "Expected date string to be RFC3339 or ISO8601-formatted."
- )
- }
- })
-
+ decoder.dateDecodingStrategy = .iso8601
return decoder
}
}
diff --git a/ios/MullvadVPN/REST/RESTDevicesProxy.swift b/ios/MullvadVPN/REST/RESTDevicesProxy.swift
index 698c51681b..5a1fa115fd 100644
--- a/ios/MullvadVPN/REST/RESTDevicesProxy.swift
+++ b/ios/MullvadVPN/REST/RESTDevicesProxy.swift
@@ -17,15 +17,80 @@ extension REST {
name: "DevicesProxy",
configuration: configuration,
requestFactory: RequestFactory.withDefaultAPICredentials(
- pathPrefix: "/accounts/v1-beta1",
+ pathPrefix: "/accounts/v1",
bodyEncoder: Coding.makeJSONEncoder()
),
- responseDecoder: ResponseDecoder(
- decoder: Coding.makeJSONDecoder()
+ responseDecoder: Coding.makeJSONDecoder()
+ )
+ }
+
+ /// Fetch device by identifier.
+ /// The completion handler receives `nil` if device is not found.
+ func getDevice(
+ accountNumber: String,
+ identifier: String,
+ retryStrategy: REST.RetryStrategy,
+ completion: @escaping CompletionHandler<Device?>
+ ) -> Cancellable
+ {
+ let requestHandler = AnyRequestHandler(
+ createURLRequest: { endpoint, authorization in
+ var path: URLPathTemplate = "devices/{id}"
+
+ try path.addPercentEncodedReplacement(
+ name: "id",
+ value: identifier,
+ allowedCharacters: .urlPathAllowed
+ )
+
+ var requestBuilder = try self.requestFactory.createRequestBuilder(
+ endpoint: endpoint,
+ method: .get,
+ pathTemplate: path
+ )
+
+ requestBuilder.setAuthorization(authorization)
+
+ return requestBuilder.getRequest()
+ },
+ authorizationProvider: createAuthorizationProvider(
+ accountNumber: accountNumber,
+ retryStrategy: .default
)
)
+
+ let responseHandler = AnyResponseHandler { response, data -> ResponseHandlerResult<Device?> in
+ let httpStatus = HTTPStatus(rawValue: response.statusCode)
+
+ switch httpStatus {
+ case let httpStatus where httpStatus.isSuccess:
+ return .decoding {
+ return try self.responseDecoder.decode(Device.self, from: data)
+ }
+
+ case .notFound:
+ return .success(nil)
+
+ default:
+ return .unhandledResponse(
+ try? self.responseDecoder.decode(
+ ServerErrorResponse.self,
+ from: data
+ )
+ )
+ }
+ }
+
+ return addOperation(
+ name: "get-device",
+ retryStrategy: retryStrategy,
+ requestHandler: requestHandler,
+ responseHandler: responseHandler,
+ completionHandler: completion
+ )
}
+ /// Fetch a list of created devices.
func getDevices(
accountNumber: String,
retryStrategy: REST.RetryStrategy,
@@ -34,27 +99,20 @@ extension REST {
{
let requestHandler = AnyRequestHandler(
createURLRequest: { endpoint, authorization in
- var requestBuilder = self.requestFactory.createURLRequestBuilder(
+ var requestBuilder = try self.requestFactory.createRequestBuilder(
endpoint: endpoint,
method: .get,
- path: "/devices"
+ pathTemplate: "devices"
)
requestBuilder.setAuthorization(authorization)
- return .success(requestBuilder.getURLRequest())
+ return requestBuilder.getRequest()
},
- requestAuthorization: { completion in
- return self.configuration.accessTokenManager
- .getAccessToken(
- accountNumber: accountNumber,
- retryStrategy: retryStrategy
- ) { operationCompletion in
- completion(operationCompletion.map { tokenData in
- return .accessToken(tokenData.accessToken)
- })
- }
- }
+ authorizationProvider: createAuthorizationProvider(
+ accountNumber: accountNumber,
+ retryStrategy: .default
+ )
)
let responseHandler = REST.defaultResponseHandler(
@@ -71,12 +129,208 @@ extension REST {
)
}
+ /// Create new device.
+ /// The completion handler will receive a `CreateDeviceResponse.created(Device)` on success.
+ /// Other `CreateDeviceResponse` variants describe errors.
+ func createDevice(
+ accountNumber: String,
+ request: CreateDeviceRequest,
+ retryStrategy: REST.RetryStrategy,
+ completion: @escaping CompletionHandler<Device>
+ ) -> Cancellable
+ {
+ let requestHandler = AnyRequestHandler(
+ createURLRequest: { endpoint, authorization in
+ var requestBuilder = try self.requestFactory.createRequestBuilder(
+ endpoint: endpoint,
+ method: .post,
+ pathTemplate: "devices"
+ )
+ requestBuilder.setAuthorization(authorization)
+
+ try requestBuilder.setHTTPBody(value: request)
+
+ return requestBuilder.getRequest()
+ },
+ authorizationProvider: createAuthorizationProvider(
+ accountNumber: accountNumber,
+ retryStrategy: .default
+ )
+ )
+
+ let responseHandler = REST.defaultResponseHandler(
+ decoding: Device.self,
+ with: responseDecoder
+ )
+
+ return addOperation(
+ name: "create-device",
+ retryStrategy: retryStrategy,
+ requestHandler: requestHandler,
+ responseHandler: responseHandler,
+ completionHandler: completion
+ )
+ }
+
+ /// Delete device by identifier.
+ /// The completion handler will receive `true` if device is successfully removed,
+ /// otherwise `false` if device is not found or already removed.
+ func deleteDevice(
+ accountNumber: String,
+ identifier: String,
+ retryStrategy: REST.RetryStrategy,
+ completion: @escaping CompletionHandler<Bool>
+ ) -> Cancellable
+ {
+ let requestHandler = AnyRequestHandler(
+ createURLRequest: { endpoint, authorization in
+ var path: URLPathTemplate = "devices/{id}"
+
+ try path.addPercentEncodedReplacement(
+ name: "id",
+ value: identifier,
+ allowedCharacters: .urlPathAllowed
+ )
+
+ var requestBuilder = try self.requestFactory
+ .createRequestBuilder(
+ endpoint: endpoint,
+ method: .delete,
+ pathTemplate: path
+ )
+
+ requestBuilder.setAuthorization(authorization)
+
+ return requestBuilder.getRequest()
+ },
+ authorizationProvider: createAuthorizationProvider(
+ accountNumber: accountNumber,
+ retryStrategy: .default
+ )
+ )
+
+ let responseHandler = AnyResponseHandler { response, data -> ResponseHandlerResult<Bool> in
+ let statusCode = HTTPStatus(rawValue: response.statusCode)
+
+ switch statusCode {
+ case let statusCode where statusCode.isSuccess:
+ return .success(true)
+
+ case .notFound:
+ return .success(false)
+
+ default:
+ return .unhandledResponse(
+ try? self.responseDecoder.decode(
+ ServerErrorResponse.self,
+ from: data
+ )
+ )
+ }
+ }
+
+ return addOperation(
+ name: "delete-device",
+ retryStrategy: retryStrategy,
+ requestHandler: requestHandler,
+ responseHandler: responseHandler,
+ completionHandler: completion
+ )
+ }
+
+ /// Rotate device key
+ func rotateDeviceKey(
+ accountNumber: String,
+ identifier: String,
+ publicKey: PublicKey,
+ retryStrategy: REST.RetryStrategy,
+ completion: @escaping CompletionHandler<Device>
+ ) -> Cancellable {
+ let requestHandler = AnyRequestHandler(
+ createURLRequest: { endpoint, authorization in
+ var path: URLPathTemplate = "devices/{id}/pubkey"
+
+ try path.addPercentEncodedReplacement(
+ name: "id",
+ value: identifier,
+ allowedCharacters: .urlPathAllowed
+ )
+
+ var requestBuilder = try self.requestFactory
+ .createRequestBuilder(
+ endpoint: endpoint,
+ method: .put,
+ pathTemplate: path
+ )
+
+ requestBuilder.setAuthorization(authorization)
+
+ let request = RotateDeviceKeyRequest(
+ publicKey: publicKey
+ )
+ try requestBuilder.setHTTPBody(value: request)
+
+ let urlRequest = requestBuilder.getRequest()
+
+ return urlRequest
+ },
+ authorizationProvider: createAuthorizationProvider(
+ accountNumber: accountNumber,
+ retryStrategy: .default
+ )
+ )
+
+ let responseHandler = REST.defaultResponseHandler(
+ decoding: Device.self,
+ with: responseDecoder
+ )
+
+ return addOperation(
+ name: "rotate-device-key",
+ retryStrategy: retryStrategy,
+ requestHandler: requestHandler,
+ responseHandler: responseHandler,
+ completionHandler: completion
+ )
+ }
+
+ }
+
+ struct CreateDeviceRequest: Encodable {
+ let publicKey: PublicKey
+ let hijackDNS: Bool
+
+ private enum CodingKeys: String, CodingKey {
+ case hijackDNS = "hijackDns"
+ case publicKey = "pubkey"
+ }
+
+ func encode(to encoder: Encoder) throws {
+ var container = encoder.container(keyedBy: CodingKeys.self)
+
+ try container.encode(publicKey.base64Key, forKey: .publicKey)
+ try container.encode(hijackDNS, forKey: .hijackDNS)
+ }
+ }
+
+ fileprivate struct RotateDeviceKeyRequest: Encodable {
+ let publicKey: PublicKey
+
+ private enum CodingKeys: String, CodingKey {
+ case publicKey = "pubkey"
+ }
+
+ func encode(to encoder: Encoder) throws {
+ var container = encoder.container(keyedBy: CodingKeys.self)
+
+ try container.encode(publicKey.base64Key, forKey: .publicKey)
+ }
}
struct Device: Decodable {
let id: String
let name: String
- let pubkey: Data
+ let pubkey: PublicKey
let hijackDNS: Bool
let created: Date
let ipv4Address: IPAddressRange
diff --git a/ios/MullvadVPN/REST/RESTError.swift b/ios/MullvadVPN/REST/RESTError.swift
index 6ee7e74d26..3b6f24c49a 100644
--- a/ios/MullvadVPN/REST/RESTError.swift
+++ b/ios/MullvadVPN/REST/RESTError.swift
@@ -12,117 +12,71 @@ extension REST {
/// An error type returned by REST API classes.
enum Error: ChainedError {
- /// A failure to encode the payload
- case encodePayload(Swift.Error)
+ /// A failure to create URL request.
+ case createURLRequest(Swift.Error)
- /// A failure during networking
+ /// A failure during networking.
case network(URLError)
- /// A failure reported by server
- case server(REST.ServerErrorResponse)
+ /// A failure to handle response.
+ case unhandledResponse(_ statusCode: Int, _ serverResponse: ServerErrorResponse?)
- /// A failure to decode the error response from server
- case decodeErrorResponse(Swift.Error)
-
- /// A failure to decode the success response from server
- case decodeSuccessResponse(Swift.Error)
+ /// A failure to decode server response.
+ case decodeResponse(Swift.Error)
var errorDescription: String? {
switch self {
- case .encodePayload:
- return "Failure to encode the payload."
+ case .createURLRequest:
+ return "Failure to create URL request."
case .network:
return "Network error."
- case .server:
- return "Server error."
- case .decodeErrorResponse:
- return "Failure to decode error response from server."
- case .decodeSuccessResponse:
- return "Failure to decode success response from server."
- }
- }
- }
+ case .unhandledResponse(let statusCode, let serverResponse):
+ var str = "Failure to handle server response: HTTP/\(statusCode)."
- /// A struct that represents a server response in case of error (any HTTP status code except 2xx).
- struct ServerErrorResponse: LocalizedError, Decodable, Equatable {
- /// A list of known server error codes
- enum Code: String, Equatable {
- case invalidAccount = "INVALID_ACCOUNT"
- case keyLimitReached = "KEY_LIMIT_REACHED"
- case pubKeyNotFound = "PUBKEY_NOT_FOUND"
- case invalidAccessToken = "INVALID_ACCESS_TOKEN"
+ if let code = serverResponse?.code {
+ str += " Error code: \(code)."
+ }
- static func ~= (pattern: Self, value: REST.ServerErrorResponse) -> Bool {
- return pattern.rawValue == value.code
+ if let detail = serverResponse?.detail {
+ str += " Detail: \(detail)."
+ }
+
+ return str
+ case .decodeResponse:
+ return "Failure to decode URL response data."
}
}
+ }
- static var invalidAccount: Code {
- return .invalidAccount
- }
- static var keyLimitReached: Code {
- return .keyLimitReached
- }
- static var pubKeyNotFound: Code {
- return .pubKeyNotFound
- }
- static var invalidAccessToken: Code {
- return .invalidAccessToken
- }
+ struct ServerErrorResponse: Decodable {
+ let code: ServerResponseCode
+ let detail: String?
- let code: String
- let error: String?
+ private enum CodingKeys: String, CodingKey {
+ case code, detail, error
+ }
- var errorDescription: String? {
- switch code {
- case Code.keyLimitReached.rawValue:
- return NSLocalizedString(
- "KEY_LIMIT_REACHED_ERROR_DESCRIPTION",
- tableName: "REST",
- value: "Too many WireGuard keys in use.",
- comment: ""
- )
- case Code.invalidAccount.rawValue:
- return NSLocalizedString(
- "INVALID_ACCOUNT_ERROR_DESCRIPTION",
- tableName: "REST",
- value: "Invalid account.",
- comment: ""
- )
+ init(from decoder: Decoder) throws {
+ let container = try decoder.container(keyedBy: CodingKeys.self)
+ let rawValue = try container.decode(String.self, forKey: .code)
- case Code.invalidAccessToken.rawValue:
- return NSLocalizedString(
- "INVALID_ACCESS_TOKEN_ERROR_DESCRIPTION",
- tableName: "REST",
- value: "Invalid access token.",
- comment: "")
- default:
- let localizedString = NSLocalizedString(
- "UNKNOWN_ERROR_DESCRIPTION",
- tableName: "REST",
- value: "Unknown error: %@",
- comment: "Use %@ placeholder to place the error code into the localized string."
- )
- return String(format: localizedString, code)
- }
+ code = ServerResponseCode(rawValue: rawValue)
+ detail = try container.decodeIfPresent(String.self, forKey: .detail)
+ ?? container.decodeIfPresent(String.self, forKey: .error)
}
+ }
- var recoverySuggestion: String? {
- switch code {
- case Code.keyLimitReached.rawValue:
- return NSLocalizedString(
- "KEY_LIMIT_REACHED_ERROR_RECOVERY_SUGGESTION",
- tableName: "REST",
- value: "Please visit the website to revoke a key before login is possible.",
- comment: ""
- )
- default:
- return nil
- }
- }
+ struct ServerResponseCode: RawRepresentable, Equatable {
+ static let invalidAccount = ServerResponseCode(rawValue: "INVALID_ACCOUNT")
+ static let keyLimitReached = ServerResponseCode(rawValue: "KEY_LIMIT_REACHED")
+ static let publicKeyNotFound = ServerResponseCode(rawValue: "PUBKEY_NOT_FOUND")
+ static let publicKeyInUse = ServerResponseCode(rawValue: "PUBKEY_IN_USE")
+ static let maxDevicesReached = ServerResponseCode(rawValue: "MAX_DEVICES_REACHED")
+ static let invalidAccessToken = ServerResponseCode(rawValue: "INVALID_ACCESS_TOKEN")
- static func == (lhs: Self, rhs: Self) -> Bool {
- return lhs.code == rhs.code
+ let rawValue: String
+ init(rawValue: String) {
+ self.rawValue = rawValue
}
}
diff --git a/ios/MullvadVPN/REST/RESTNetworkOperation.swift b/ios/MullvadVPN/REST/RESTNetworkOperation.swift
index 97e7c0e9a6..14b441fca9 100644
--- a/ios/MullvadVPN/REST/RESTNetworkOperation.swift
+++ b/ios/MullvadVPN/REST/RESTNetworkOperation.swift
@@ -15,6 +15,7 @@ extension REST {
private let responseHandler: AnyResponseHandler<Success>
private let dispatchQueue: DispatchQueue
+ private let logger: Logger
private let urlSession: URLSession
private let addressCacheStore: AddressCache.Store
@@ -28,9 +29,6 @@ extension REST {
private var retryTimer: DispatchSourceTimer?
private var retryCount = 0
- private let logger = Logger(label: "REST.NetworkOperation")
- private let loggerMetadata: Logger.Metadata
-
init(
name: String,
dispatchQueue: DispatchQueue,
@@ -48,7 +46,9 @@ extension REST {
self.requestHandler = requestHandler
self.responseHandler = responseHandler
- loggerMetadata = ["name": .string(name)]
+ var logger = Logger(label: "REST.NetworkOperation")
+ logger[metadataKey: "name"] = .string(name)
+ self.logger = logger
super.init(completionQueue: .main, completionHandler: completionHandler)
}
@@ -81,10 +81,15 @@ extension REST {
return
}
- let authorizationResult = requestHandler.requestAuthorization { completion in
- self.dispatchQueue.async {
- assert(self.requiresAuthorization, "Illegal use of completion handler.")
+ guard let authorizationProvider = requestHandler.authorizationProvider else {
+ requiresAuthorization = false
+ didReceiveAuthorization(nil)
+ return
+ }
+ requiresAuthorization = true
+ authorizationTask = authorizationProvider.getAuthorization { completion in
+ self.dispatchQueue.async {
switch completion {
case .success(let authorization):
self.didReceiveAuthorization(authorization)
@@ -97,16 +102,6 @@ extension REST {
}
}
}
-
- switch authorizationResult {
- case .pending(let task):
- requiresAuthorization = true
- authorizationTask = task
-
- case .noRequirement:
- requiresAuthorization = false
- didReceiveAuthorization(nil)
- }
}
private func didReceiveAuthorization(_ authorization: REST.Authorization?) {
@@ -119,17 +114,15 @@ extension REST {
let endpoint = self.addressCacheStore.getCurrentEndpoint()
- let result = requestHandler.createURLRequest(
- endpoint: endpoint,
- authorization: authorization
- )
+ do {
+ let request = try requestHandler.createURLRequest(
+ endpoint: endpoint,
+ authorization: authorization
+ )
- switch result {
- case .success(let request):
didReceiveURLRequest(request, endpoint: endpoint)
-
- case .failure(let error):
- didFailToCreateURLRequest(error)
+ } catch {
+ didFailToCreateURLRequest(.createURLRequest(error))
}
}
@@ -138,22 +131,18 @@ extension REST {
logger.error(
chainedError: error,
- message: "Failed to request authorization.",
- metadata: loggerMetadata
+ message: "Failed to request authorization."
)
finish(completion: .failure(error))
}
- private func didReceiveURLRequest(_ urlRequest: URLRequest, endpoint: AnyIPEndpoint) {
+ private func didReceiveURLRequest(_ restRequest: REST.Request, endpoint: AnyIPEndpoint) {
dispatchPrecondition(condition: .onQueue(dispatchQueue))
- logger.debug(
- "Executing request using \(endpoint).",
- metadata: loggerMetadata
- )
+ logger.debug("Send request to \(restRequest.pathTemplate.templateString) via \(endpoint).")
- networkTask = urlSession.dataTask(with: urlRequest) { [weak self] data, response, error in
+ networkTask = urlSession.dataTask(with: restRequest.urlRequest) { [weak self] data, response, error in
guard let self = self else { return }
self.dispatchQueue.async {
@@ -178,8 +167,7 @@ extension REST {
logger.error(
chainedError: error,
- message: "Failed to create URLRequest.",
- metadata: loggerMetadata
+ message: "Failed to create URLRequest."
)
finish(completion: .failure(error))
@@ -202,17 +190,13 @@ extension REST {
logger.error(
chainedError: AnyChainedError(urlError),
- message: "Failed to perform request to \(endpoint).",
- metadata: loggerMetadata
+ message: "Failed to perform request to \(endpoint)."
)
// Check if retry count is not exceeded.
guard retryCount < retryStrategy.maxRetryCount else {
if retryStrategy.maxRetryCount > 0 {
- logger.debug(
- "Ran out of retry attempts (\(retryStrategy.maxRetryCount))",
- metadata: loggerMetadata
- )
+ logger.debug("Ran out of retry attempts (\(retryStrategy.maxRetryCount))")
}
finish(completion: OperationCompletion(result: .failure(.network(urlError))))
@@ -248,19 +232,39 @@ extension REST {
private func didReceiveURLResponse(_ response: HTTPURLResponse, data: Data, endpoint: AnyIPEndpoint) {
dispatchPrecondition(condition: .onQueue(dispatchQueue))
- let result = responseHandler.handleURLResponse(response, data: data)
+ logger.debug("Response: \(response.statusCode).")
- if case .server(.invalidAccessToken) = result.error,
- requiresAuthorization, retryInvalidAccessTokenError
- {
- logger.debug(
- "Received invalid access token error. Retry once.",
- metadata: loggerMetadata
- )
- retryInvalidAccessTokenError = false
- startRequest()
- } else {
- finish(completion: OperationCompletion(result: result))
+ let handlerResult = responseHandler.handleURLResponse(response, data: data)
+
+ switch handlerResult {
+ case .success(let output):
+ // Response handler produced value.
+ finish(completion: .success(output))
+
+ case .decoding(let decoderBlock):
+ // Response handler returned a block decoding value.
+ let decodeResult = Result { try decoderBlock() }
+ .mapError { error -> REST.Error in
+ return .decodeResponse(error)
+ }
+ finish(completion: OperationCompletion(result: decodeResult))
+
+ case .unhandledResponse(let serverErrorResponse):
+ // Response handler couldn't handle the response.
+ if serverErrorResponse?.code == .invalidAccessToken,
+ requiresAuthorization,
+ retryInvalidAccessTokenError
+ {
+ logger.debug("Received invalid access token error. Retry once.")
+ retryInvalidAccessTokenError = false
+ startRequest()
+ } else {
+ finish(
+ completion: .failure(
+ .unhandledResponse(response.statusCode, serverErrorResponse)
+ )
+ )
+ }
}
}
}
diff --git a/ios/MullvadVPN/REST/RESTProxy.swift b/ios/MullvadVPN/REST/RESTProxy.swift
index 611f4d9c34..f166338976 100644
--- a/ios/MullvadVPN/REST/RESTProxy.swift
+++ b/ios/MullvadVPN/REST/RESTProxy.swift
@@ -25,13 +25,13 @@ extension REST {
let requestFactory: REST.RequestFactory
/// URL response decoder.
- let responseDecoder: REST.ResponseDecoder
+ let responseDecoder: JSONDecoder
init(
name: String,
configuration: ConfigurationType,
requestFactory: REST.RequestFactory,
- responseDecoder: REST.ResponseDecoder
+ responseDecoder: JSONDecoder
)
{
dispatchQueue = DispatchQueue(label: "REST.\(name).dispatchQueue")
diff --git a/ios/MullvadVPN/REST/RESTRequestFactory.swift b/ios/MullvadVPN/REST/RESTRequestFactory.swift
index 064af103fd..2046f26daa 100644
--- a/ios/MullvadVPN/REST/RESTRequestFactory.swift
+++ b/ios/MullvadVPN/REST/RESTRequestFactory.swift
@@ -37,14 +37,15 @@ extension REST {
self.bodyEncoder = bodyEncoder
}
- func createURLRequest(endpoint: AnyIPEndpoint, method: HTTPMethod, path: String) -> URLRequest {
+ func createRequest(endpoint: AnyIPEndpoint, method: HTTPMethod, pathTemplate: URLPathTemplate) throws -> REST.Request {
var urlComponents = URLComponents()
urlComponents.scheme = "https"
urlComponents.path = pathPrefix
urlComponents.host = "\(endpoint.ip)"
urlComponents.port = Int(endpoint.port)
- let requestURL = urlComponents.url!.appendingPathComponent(path)
+ let pathString = try pathTemplate.pathString()
+ let requestURL = urlComponents.url!.appendingPathComponent(pathString)
var request = URLRequest(
url: requestURL,
@@ -55,38 +56,44 @@ extension REST {
request.addValue(hostname, forHTTPHeaderField: HTTPHeader.host)
request.addValue("application/json", forHTTPHeaderField: HTTPHeader.contentType)
request.httpMethod = method.rawValue
- return request
+
+ let prefixedPathTemplate = URLPathTemplate(stringLiteral: pathPrefix) + pathTemplate
+
+ return REST.Request(
+ urlRequest: request,
+ pathTemplate: prefixedPathTemplate
+ )
}
- func createURLRequestBuilder(
+ func createRequestBuilder(
endpoint: AnyIPEndpoint,
method: HTTPMethod,
- path: String
- ) -> RequestBuilder {
- let request = createURLRequest(
+ pathTemplate: URLPathTemplate
+ ) throws -> RequestBuilder {
+ let request = try createRequest(
endpoint: endpoint,
method: method,
- path: path
+ pathTemplate: pathTemplate
)
return RequestBuilder(
- request: request,
+ restRequest: request,
bodyEncoder: bodyEncoder
)
}
}
struct RequestBuilder {
- private var request: URLRequest
+ private var restRequest: REST.Request
private let bodyEncoder: JSONEncoder
- init(request: URLRequest, bodyEncoder: JSONEncoder) {
- self.request = request
+ init(restRequest: REST.Request, bodyEncoder: JSONEncoder) {
+ self.restRequest = restRequest
self.bodyEncoder = bodyEncoder
}
mutating func setHTTPBody<T: Encodable>(value: T) throws {
- request.httpBody = try bodyEncoder.encode(value)
+ restRequest.urlRequest.httpBody = try bodyEncoder.encode(value)
}
mutating func setETagHeader(etag: String) {
@@ -95,7 +102,7 @@ extension REST {
if etag.starts(with: "\"") {
etag.insert(contentsOf: "W/", at: etag.startIndex)
}
- request.setValue(etag, forHTTPHeaderField: HTTPHeader.ifNoneMatch)
+ restRequest.urlRequest.setValue(etag, forHTTPHeaderField: HTTPHeader.ifNoneMatch)
}
mutating func setAuthorization(_ authorization: REST.Authorization) {
@@ -108,11 +115,119 @@ extension REST {
value = "Bearer \(accessToken)"
}
- request.addValue(value, forHTTPHeaderField: HTTPHeader.authorization)
+ restRequest.urlRequest.addValue(value, forHTTPHeaderField: HTTPHeader.authorization)
}
- func getURLRequest() -> URLRequest {
- return request
+ func getRequest() -> REST.Request {
+ return restRequest
}
}
+
+ struct URLPathTemplate: ExpressibleByStringLiteral {
+ enum Component {
+ case literal(String)
+ case placeholder(String)
+ }
+
+ enum Error: LocalizedError {
+ /// Replacement value is not provided for placeholder.
+ case noReplacement(_ name: String)
+
+ /// Failure to perecent encode replacement value.
+ case percentEncoding
+
+ var errorDescription: String? {
+ switch self {
+ case .noReplacement(let placeholder):
+ return "Replacement is not provided for \(placeholder)."
+
+ case .percentEncoding:
+ return "Failed to percent encode replacement value."
+ }
+ }
+ }
+
+ private var components: [Component]
+ private var replacements = [String: String]()
+
+ init(stringLiteral value: StringLiteralType) {
+ let slashCharset = CharacterSet(charactersIn: "/")
+
+ components = value.split(separator: "/").map { subpath -> Component in
+ if subpath.hasPrefix("{") && subpath.hasSuffix("}") {
+ let name = String(subpath.dropFirst().dropLast())
+
+ return .placeholder(name)
+ } else {
+ return .literal(
+ subpath.trimmingCharacters(in: slashCharset)
+ )
+ }
+ }
+ }
+
+ private init(components: [Component]) {
+ self.components = components
+ }
+
+ mutating func addPercentEncodedReplacement(
+ name: String,
+ value: String,
+ allowedCharacters: CharacterSet
+ ) throws {
+ let encoded = value.addingPercentEncoding(
+ withAllowedCharacters: allowedCharacters
+ )
+
+ if let encoded = encoded {
+ replacements[name] = encoded
+ } else {
+ throw Error.percentEncoding
+ }
+ }
+
+ var templateString: String {
+ var combinedString = ""
+
+ for component in components {
+ combinedString += "/"
+
+ switch component {
+ case .literal(let string):
+ combinedString += string
+ case .placeholder(let name):
+ combinedString += "{\(name)}"
+ }
+ }
+
+ return combinedString
+ }
+
+ func pathString() throws -> String {
+ var combinedPath = ""
+
+ for component in components {
+ combinedPath += "/"
+
+ switch component {
+ case .literal(let string):
+ combinedPath += string
+
+ case .placeholder(let name):
+ if let string = replacements[name] {
+ combinedPath += string
+ } else {
+ throw Error.noReplacement(name)
+ }
+ }
+ }
+
+ return combinedPath
+ }
+
+ static func + (lhs: URLPathTemplate, rhs: URLPathTemplate) -> URLPathTemplate {
+ return URLPathTemplate(components: lhs.components + rhs.components)
+ }
+ }
+
}
diff --git a/ios/MullvadVPN/REST/RESTRequestHandler.swift b/ios/MullvadVPN/REST/RESTRequestHandler.swift
index d0f7b54dc3..6d690d19e7 100644
--- a/ios/MullvadVPN/REST/RESTRequestHandler.swift
+++ b/ios/MullvadVPN/REST/RESTRequestHandler.swift
@@ -9,59 +9,47 @@
import Foundation
protocol RESTRequestHandler {
- typealias AuthorizationCompletion = (OperationCompletion<REST.Authorization, REST.Error>) -> Void
+ func createURLRequest(
+ endpoint: AnyIPEndpoint,
+ authorization: REST.Authorization?
+ ) throws -> REST.Request
- func createURLRequest(endpoint: AnyIPEndpoint, authorization: REST.Authorization?) -> Result<URLRequest, REST.Error>
- func requestAuthorization(completion: @escaping AuthorizationCompletion) -> REST.AuthorizationResult
+ var authorizationProvider: RESTAuthorizationProvider? { get }
}
extension REST {
-
- enum AuthorizationResult {
- /// There is no requirement for authorizing this request.
- case noRequirement
-
- /// Authorization request is initiated.
- /// Associated value contains a handle that can be used to cancel
- /// the request.
- case pending(Cancellable)
+ struct Request {
+ var urlRequest: URLRequest
+ var pathTemplate: URLPathTemplate
}
final class AnyRequestHandler: RESTRequestHandler {
- private let _createURLRequest: (AnyIPEndpoint, REST.Authorization?) -> Result<URLRequest, REST.Error>
- private let _requestAuthorization: ((@escaping AuthorizationCompletion) -> AuthorizationResult)?
+ private let _createURLRequest: (AnyIPEndpoint, REST.Authorization?) throws -> REST.Request
- init(createURLRequest: @escaping (AnyIPEndpoint) -> Result<URLRequest, REST.Error>) {
+ let authorizationProvider: RESTAuthorizationProvider?
+
+ init(createURLRequest: @escaping (AnyIPEndpoint) throws -> REST.Request) {
_createURLRequest = { endpoint, authorization in
- createURLRequest(endpoint)
+ return try createURLRequest(endpoint)
}
- _requestAuthorization = nil
+ authorizationProvider = nil
}
init(
- createURLRequest: @escaping (AnyIPEndpoint, REST.Authorization) -> Result<URLRequest, REST.Error>,
- requestAuthorization: @escaping (@escaping AuthorizationCompletion) -> Cancellable
+ createURLRequest: @escaping (AnyIPEndpoint, REST.Authorization) throws -> REST.Request,
+ authorizationProvider: RESTAuthorizationProvider
) {
_createURLRequest = { endpoint, authorization in
- return createURLRequest(endpoint, authorization!)
- }
- _requestAuthorization = { completion in
- return .pending(requestAuthorization(completion))
+ return try createURLRequest(endpoint, authorization!)
}
+ self.authorizationProvider = authorizationProvider
}
func createURLRequest(
endpoint: AnyIPEndpoint,
authorization: REST.Authorization?
- ) -> Result<URLRequest, REST.Error> {
- return _createURLRequest(endpoint, authorization)
- }
-
- func requestAuthorization(
- completion: @escaping (OperationCompletion<REST.Authorization, REST.Error>) -> Void
- ) -> REST.AuthorizationResult {
- return _requestAuthorization?(completion) ?? .noRequirement
+ ) throws -> REST.Request {
+ return try _createURLRequest(endpoint, authorization)
}
}
-
}
diff --git a/ios/MullvadVPN/REST/RESTResponseDecoder.swift b/ios/MullvadVPN/REST/RESTResponseDecoder.swift
deleted file mode 100644
index 2e79bbc9e3..0000000000
--- a/ios/MullvadVPN/REST/RESTResponseDecoder.swift
+++ /dev/null
@@ -1,46 +0,0 @@
-//
-// RESTResponseDecoder.swift
-// MullvadVPN
-//
-// Created by pronebird on 16/04/2022.
-// Copyright © 2022 Mullvad VPN AB. All rights reserved.
-//
-
-import Foundation
-
-extension REST {
- struct ResponseDecoder {
- let decoder: JSONDecoder
-
- init(decoder: JSONDecoder) {
- self.decoder = decoder
- }
-
- // Parse JSON response into the given `Decodable` type.
- func decodeSuccessResponse<T: Decodable>(_ type: T.Type, from data: Data) -> Result<T, REST.Error> {
- return Result { try decoder.decode(type, from: data) }
- .mapError { error in
- return .decodeSuccessResponse(error)
- }
- }
-
- /// Parse server error response from JSON.
- func decodeErrorResponse(from data: Data) -> Result<REST.ServerErrorResponse, REST.Error> {
- return Result { () -> REST.ServerErrorResponse in
- return try decoder.decode(REST.ServerErrorResponse.self, from: data)
- }
- .mapError { error in
- return .decodeErrorResponse(error)
- }
- }
-
- /// Parse server error response from JSON and map it to `RESTError.server` error kind.
- func decodeErrorResponseAndMapToServerError<T>(from data: Data) -> Result<T, REST.Error> {
- return decodeErrorResponse(from: data)
- .flatMap { serverError in
- return .failure(.server(serverError))
- }
- }
- }
-
-}
diff --git a/ios/MullvadVPN/REST/RESTResponseHandler.swift b/ios/MullvadVPN/REST/RESTResponseHandler.swift
index 65ae4b6e2d..1ec56541fa 100644
--- a/ios/MullvadVPN/REST/RESTResponseHandler.swift
+++ b/ios/MullvadVPN/REST/RESTResponseHandler.swift
@@ -11,12 +11,25 @@ import Foundation
protocol RESTResponseHandler {
associatedtype Success
- func handleURLResponse(_ response: HTTPURLResponse, data: Data) -> Result<Success, REST.Error>
+ func handleURLResponse(_ response: HTTPURLResponse, data: Data) -> REST.ResponseHandlerResult<Success>
}
extension REST {
+ /// Responser handler result type.
+ enum ResponseHandlerResult<Success> {
+ /// Response handler succeeded and produced a value.
+ case success(Success)
+
+ /// Response handler succeeded and returned a block that decodes the value.
+ case decoding(_ decoderBlock: () throws -> Success)
+
+ /// Response handler received the response that it cannot handle.
+ /// Server error response is attached when available.
+ case unhandledResponse(ServerErrorResponse?)
+ }
+
final class AnyResponseHandler<Success>: RESTResponseHandler {
- typealias HandlerBlock = (HTTPURLResponse, Data) -> Result<Success, REST.Error>
+ typealias HandlerBlock = (HTTPURLResponse, Data) -> REST.ResponseHandlerResult<Success>
private let handlerBlock: HandlerBlock
@@ -24,7 +37,7 @@ extension REST {
handlerBlock = block
}
- func handleURLResponse(_ response: HTTPURLResponse, data: Data) -> Result<Success, REST.Error> {
+ func handleURLResponse(_ response: HTTPURLResponse, data: Data) -> REST.ResponseHandlerResult<Success> {
return handlerBlock(response, data)
}
}
@@ -32,12 +45,22 @@ extension REST {
/// Returns default response handler that parses JSON response into the
/// given `Decodable` type when it encounters HTTP `2xx` code, otherwise
/// attempts to decode the server error.
- static func defaultResponseHandler<T: Decodable>(decoding type: T.Type, with decoder: REST.ResponseDecoder) -> AnyResponseHandler<T> {
+ static func defaultResponseHandler<T: Decodable>(
+ decoding type: T.Type,
+ with decoder: JSONDecoder
+ ) -> AnyResponseHandler<T> {
return AnyResponseHandler { response, data in
if HTTPStatus.isSuccess(response.statusCode) {
- return decoder.decodeSuccessResponse(type, from: data)
+ return .decoding {
+ try decoder.decode(type, from: data)
+ }
} else {
- return decoder.decodeErrorResponseAndMapToServerError(from: data)
+ return .unhandledResponse(
+ try? decoder.decode(
+ ServerErrorResponse.self,
+ from: data
+ )
+ )
}
}
}
diff --git a/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift b/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift
index 6952a4130e..a38a23bb6c 100644
--- a/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift
+++ b/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift
@@ -158,7 +158,8 @@ class SetAccountOperation: ResultOperation<(), TunnelManager.Error> {
case .success:
self.logger.info("Removed key (\(index)) from server.")
- case .failure(.server(.pubKeyNotFound)):
+ case .failure(.unhandledResponse(_, let serverErrorResponse))
+ where serverErrorResponse?.code == .publicKeyNotFound:
self.logger.debug("Key (\(index)) was not found on server.")
case .failure(let error):
diff --git a/ios/MullvadVPN/TunnelManager/TunnelManager.swift b/ios/MullvadVPN/TunnelManager/TunnelManager.swift
index 76f7c0945b..755e92306c 100644
--- a/ios/MullvadVPN/TunnelManager/TunnelManager.swift
+++ b/ios/MullvadVPN/TunnelManager/TunnelManager.swift
@@ -845,7 +845,8 @@ extension TunnelManager {
// Do not retry if logged out.
return nil
- case .replaceWireguardKey(.server(.invalidAccount)):
+ case .replaceWireguardKey(.unhandledResponse(_, let serverErrorResponse))
+ where serverErrorResponse?.code == .invalidAccount:
// Do not retry if account was removed.
return nil
diff --git a/ios/MullvadVPN/WireguardKeysViewController.swift b/ios/MullvadVPN/WireguardKeysViewController.swift
index d53739edae..f3fad9cbc6 100644
--- a/ios/MullvadVPN/WireguardKeysViewController.swift
+++ b/ios/MullvadVPN/WireguardKeysViewController.swift
@@ -227,7 +227,8 @@ class WireguardKeysViewController: UIViewController, TunnelObserver {
self.updateViewState(.verifiedKey(true))
case .failure(let error):
- if case .server(.pubKeyNotFound) = error {
+ if case .unhandledResponse(_, let serverErrorResponse) = error,
+ serverErrorResponse?.code == .publicKeyNotFound {
self.updateViewState(.verifiedKey(false))
} else {
self.showKeyVerificationFailureAlert(error)