summaryrefslogtreecommitdiffhomepage
path: root/ios/MullvadVPN/GeneralAPIs/OutgoingConnectionProxy.swift
blob: ed54e4743b1e714028e00fd57b5b63715d999517 (plain)
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
//
//  OutgoingConnectionProxy.swift
//  MullvadREST
//
//  Created by Mojgan on 2023-10-24.
//  Copyright © 2025 Mullvad VPN AB. All rights reserved.
//

import Foundation
import MullvadREST
import MullvadTypes
import Network

protocol OutgoingConnectionHandling {
    func getIPV6(retryStrategy: REST.RetryStrategy) async throws -> IPV6ConnectionData
    func getIPV4(retryStrategy: REST.RetryStrategy) async throws -> IPV4ConnectionData
}

final class OutgoingConnectionProxy: OutgoingConnectionHandling {
    enum ExitIPVersion: String {
        case v4 = "ipv4"
        case v6 = "ipv6"

        func host(hostname: String) -> String {
            "\(rawValue).am.i.\(hostname)"
        }
    }

    let urlSession: URLSessionProtocol
    let hostname: String

    init(urlSession: URLSessionProtocol, hostname: String) {
        self.urlSession = urlSession
        self.hostname = hostname
    }

    func getIPV6(retryStrategy: REST.RetryStrategy) async throws -> IPV6ConnectionData {
        try await perform(retryStrategy: retryStrategy, version: .v6)
    }

    func getIPV4(retryStrategy: REST.RetryStrategy) async throws -> IPV4ConnectionData {
        try await perform(retryStrategy: retryStrategy, version: .v4)
    }

    private func perform<T: Decodable>(retryStrategy: REST.RetryStrategy, version: ExitIPVersion) async throws -> T {
        let delayIterator = retryStrategy.makeDelayIterator()
        for _ in 0..<retryStrategy.maxRetryCount {
            do {
                return try await perform(host: version.host(hostname: hostname))
            } catch {
                // ignore if request is cancelled
                if case URLError.cancelled = error {
                    throw error
                } else {
                    // retry with the delay
                    guard let delay = delayIterator.next() else { throw error }
                    let mills = UInt64(max(0, delay.milliseconds))
                    let nanos = mills.saturatingMultiplication(1_000_000)
                    try await Task.sleep(nanoseconds: nanos)
                }
            }
        }
        return try await perform(host: version.host(hostname: hostname))
    }

    private func perform<T: Decodable>(host: String) async throws -> T {
        var urlComponents = URLComponents()
        urlComponents.scheme = "https"
        urlComponents.host = host
        urlComponents.path = "/json"

        guard let url = urlComponents.url else {
            throw REST.Error.network(URLError(.badURL))
        }
        let request = URLRequest(
            url: url,
            cachePolicy: .useProtocolCachePolicy,
            timeoutInterval: REST.defaultAPINetworkTimeout.timeInterval
        )
        let (data, response) = try await data(for: request)
        guard let httpResponse = response as? HTTPURLResponse else {
            throw REST.Error.network(URLError(.badServerResponse))
        }
        let decoder = JSONDecoder()
        guard (200..<300).contains(httpResponse.statusCode) else {
            throw REST.Error.unhandledResponse(
                httpResponse.statusCode,
                try? decoder.decode(
                    REST.ServerErrorResponse.self,
                    from: data
                )
            )
        }
        let connectionData = try decoder.decode(T.self, from: data)
        return connectionData
    }
}

extension OutgoingConnectionProxy: URLSessionProtocol {
    func data(for request: URLRequest) async throws -> (Data, URLResponse) {
        return try await urlSession.data(for: request)
    }
}