2025-06-04 11:33:17 +02:00

316 lines
12 KiB
Swift

//
// RESTAPIProxy.swift
// MullvadREST
//
// Created by pronebird on 10/07/2020.
// Copyright © 2025 Mullvad VPN AB. All rights reserved.
//
import Foundation
import MullvadRustRuntime
import MullvadTypes
import Operations
import WireGuardKitTypes
extension REST {
public final class APIProxy: Proxy<AuthProxyConfiguration>, APIQuerying, @unchecked Sendable {
public init(configuration: AuthProxyConfiguration) {
super.init(
name: "APIProxy",
configuration: configuration,
requestFactory: RequestFactory.withDefaultAPICredentials(
pathPrefix: "/app/v1",
bodyEncoder: Coding.makeJSONEncoder()
),
responseDecoder: Coding.makeJSONDecoder()
)
}
public func getAddressList(
retryStrategy: REST.RetryStrategy,
completionHandler: @escaping @Sendable ProxyCompletionHandler<[AnyIPEndpoint]>
) -> Cancellable {
let requestHandler = AnyRequestHandler { endpoint in
try self.requestFactory.createRequest(
endpoint: endpoint,
method: .get,
pathTemplate: "api-addrs"
)
}
let responseHandler = REST.defaultResponseHandler(
decoding: [AnyIPEndpoint].self,
with: responseDecoder
)
let executor = makeRequestExecutor(
name: "get-api-addrs",
requestHandler: requestHandler,
responseHandler: responseHandler
)
return executor.execute(retryStrategy: retryStrategy, completionHandler: completionHandler)
}
public func getRelays(
etag: String?,
retryStrategy: REST.RetryStrategy,
completionHandler: @escaping @Sendable ProxyCompletionHandler<ServerRelaysCacheResponse>
) -> Cancellable {
let requestHandler = AnyRequestHandler { endpoint in
var requestBuilder = try self.requestFactory.createRequestBuilder(
endpoint: endpoint,
method: .get,
pathTemplate: "relays"
)
if let etag {
requestBuilder.setETagHeader(etag: etag)
}
return requestBuilder.getRequest()
}
let responseHandler =
AnyResponseHandler { response, data -> ResponseHandlerResult<ServerRelaysCacheResponse> in
let httpStatus = HTTPStatus(rawValue: response.statusCode)
switch httpStatus {
case let httpStatus where httpStatus.isSuccess:
return .decoding {
// Discarding result since we're only interested in knowing that it's parseable.
_ = try self.responseDecoder.decode(
ServerRelaysResponse.self,
from: data
)
let newEtag = response.value(forHTTPHeaderField: HTTPHeader.etag)
return .newContent(newEtag, data)
}
case .notModified where etag != nil:
return .success(.notModified)
default:
return .unhandledResponse(
try? self.responseDecoder.decode(
ServerErrorResponse.self,
from: data
)
)
}
}
let executor = makeRequestExecutor(
name: "get-relays",
requestHandler: requestHandler,
responseHandler: responseHandler
)
return executor.execute(retryStrategy: retryStrategy, completionHandler: completionHandler)
}
public func createApplePayment(
accountNumber: String,
receiptString: Data
) -> any RESTRequestExecutor<CreateApplePaymentResponse> {
let requestHandler = AnyRequestHandler(
createURLRequest: { endpoint, authorization in
var requestBuilder = try self.requestFactory.createRequestBuilder(
endpoint: endpoint,
method: .post,
pathTemplate: "create-apple-payment"
)
requestBuilder.setAuthorization(authorization)
let body = CreateApplePaymentRequest(
receiptString: receiptString
)
try requestBuilder.setHTTPBody(value: body)
return requestBuilder.getRequest()
},
authorizationProvider: createAuthorizationProvider(accountNumber: accountNumber)
)
let responseHandler =
AnyResponseHandler { response, data -> ResponseHandlerResult<CreateApplePaymentResponse> in
if HTTPStatus.isSuccess(response.statusCode) {
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 .unhandledResponse(
try? self.responseDecoder.decode(
ServerErrorResponse.self,
from: data
)
)
}
}
return makeRequestExecutor(
name: "create-apple-payment",
requestHandler: requestHandler,
responseHandler: responseHandler
)
}
public func sendProblemReport(
_ body: ProblemReportRequest,
retryStrategy: REST.RetryStrategy,
completionHandler: @escaping ProxyCompletionHandler<Void>
) -> Cancellable {
let requestHandler = AnyRequestHandler { endpoint in
var requestBuilder = try self.requestFactory.createRequestBuilder(
endpoint: endpoint,
method: .post,
pathTemplate: "problem-report"
)
try requestBuilder.setHTTPBody(value: body)
return requestBuilder.getRequest()
}
let responseHandler =
AnyResponseHandler { response, data -> ResponseHandlerResult<Void> in
if HTTPStatus.isSuccess(response.statusCode) {
return .success(())
} else {
return .unhandledResponse(
try? self.responseDecoder.decode(
ServerErrorResponse.self,
from: data
)
)
}
}
let executor = makeRequestExecutor(
name: "send-problem-report",
requestHandler: requestHandler,
responseHandler: responseHandler
)
return executor.execute(retryStrategy: retryStrategy, completionHandler: completionHandler)
}
public func submitVoucher(
voucherCode: String,
accountNumber: String,
retryStrategy: REST.RetryStrategy,
completionHandler: @escaping @Sendable ProxyCompletionHandler<SubmitVoucherResponse>
) -> Cancellable {
let requestHandler = AnyRequestHandler(
createURLRequest: { endpoint, authorization in
var requestBuilder = try self.requestFactory.createRequestBuilder(
endpoint: endpoint,
method: .post,
pathTemplate: "submit-voucher"
)
requestBuilder.setAuthorization(authorization)
try requestBuilder.setHTTPBody(value: SubmitVoucherRequest(voucherCode: voucherCode))
return requestBuilder.getRequest()
},
authorizationProvider: createAuthorizationProvider(accountNumber: accountNumber)
)
let responseHandler = AnyResponseHandler { response, data -> ResponseHandlerResult<SubmitVoucherResponse> in
if HTTPStatus.isSuccess(response.statusCode) {
return .decoding {
try self.responseDecoder.decode(SubmitVoucherResponse.self, from: data)
}
} else {
return .unhandledResponse(
try? self.responseDecoder.decode(ServerErrorResponse.self, from: data)
)
}
}
let executor = makeRequestExecutor(
name: "submit-voucher",
requestHandler: requestHandler,
responseHandler: responseHandler
)
return executor.execute(retryStrategy: retryStrategy, completionHandler: completionHandler)
}
/// Not implemented. Use `MullvadAPIProxy` instead.
public func legacyStorekitPayment(
accountNumber: String,
request: LegacyStorekitRequest,
retryStrategy: REST.RetryStrategy,
completionHandler: @escaping ProxyCompletionHandler<REST.CreateApplePaymentResponse>
) -> any Cancellable {
AnyCancellable()
}
/// Not implemented. Use `MullvadAPIProxy` instead.
public func initStorekitPayment(
accountNumber: String,
retryStrategy: REST.RetryStrategy,
completionHandler: @escaping ProxyCompletionHandler<String>
) -> any Cancellable {
AnyCancellable()
}
/// Not implemented. Use `MullvadAPIProxy` instead.
public func checkStorekitPayment(
accountNumber: String,
transaction: StorekitTransaction,
retryStrategy: REST.RetryStrategy,
completionHandler: @escaping ProxyCompletionHandler<Void>
) -> any Cancellable {
AnyCancellable()
}
public func checkApiAvailability(
retryStrategy: REST.RetryStrategy,
accessMethod: PersistentAccessMethod,
completion: @escaping ProxyCompletionHandler<Bool>
) -> any Cancellable {
AnyCancellable()
}
}
// MARK: - Response types
private struct CreateApplePaymentRequest: Encodable, Sendable {
let receiptString: Data
}
private struct CreateApplePaymentRawResponse: Decodable, Sendable {
let timeAdded: Int
let newExpiry: Date
}
private struct SubmitVoucherRequest: Encodable, Sendable {
let voucherCode: String
}
public struct SubmitVoucherResponse: Decodable, Sendable {
public let timeAdded: Int
public let newExpiry: Date
public var dateComponents: DateComponents {
DateComponents(second: timeAdded)
}
}
}