krz/domain-dig

an ios app for DNS & SSL analysis

clone: git clone https://gitbay.org/krz/domain-dig.git

v1.7.1: DomainDig/RedirectChainService.swift · raw

 1import Foundation
 2
 3struct RedirectChainService {
 4    static func trace(domain: String) async throws -> [RedirectHop] {
 5        // Try HTTPS first (avoids ATS issues), fall back to HTTP if it fails entirely
 6        do {
 7            return try await followChain(startingURL: URL(string: "https://\(domain)")!)
 8        } catch {
 9            return try await followChain(startingURL: URL(string: "http://\(domain)")!)
10        }
11    }
12
13    private static func followChain(startingURL: URL) async throws -> [RedirectHop] {
14        let delegate = NoRedirectDelegate()
15        let session = URLSession(
16            configuration: .ephemeral,
17            delegate: delegate,
18            delegateQueue: nil
19        )
20        defer { session.invalidateAndCancel() }
21
22        var hops: [RedirectHop] = []
23        var currentURL = startingURL
24        let maxRedirects = 10
25
26        for step in 1...maxRedirects + 1 {
27            var request = URLRequest(url: currentURL, timeoutInterval: 10)
28            request.httpMethod = "GET"
29
30            let (_, response) = try await session.data(for: request)
31
32            guard let httpResponse = response as? HTTPURLResponse else {
33                throw URLError(.badServerResponse)
34            }
35
36            let statusCode = httpResponse.statusCode
37            let isRedirect = (300...399).contains(statusCode)
38
39            if isRedirect, let location = httpResponse.value(forHTTPHeaderField: "Location") {
40                hops.append(RedirectHop(
41                    stepNumber: step,
42                    statusCode: statusCode,
43                    url: currentURL.absoluteString,
44                    isFinal: false
45                ))
46
47                // Resolve relative redirects
48                if let nextURL = URL(string: location, relativeTo: currentURL)?.absoluteURL {
49                    currentURL = nextURL
50                } else {
51                    break
52                }
53
54                if step > maxRedirects { break }
55            } else {
56                // Non-redirect  this is the final destination
57                hops.append(RedirectHop(
58                    stepNumber: step,
59                    statusCode: statusCode,
60                    url: currentURL.absoluteString,
61                    isFinal: true
62                ))
63                break
64            }
65        }
66
67        return hops
68    }
69}
70
71private final class NoRedirectDelegate: NSObject, URLSessionTaskDelegate, @unchecked Sendable {
72    func urlSession(
73        _ _: URLSession,
74        task _: URLSessionTask,
75        willPerformHTTPRedirection _: HTTPURLResponse,
76        newRequest _: URLRequest,
77        completionHandler: @escaping (URLRequest?) -> Void
78    ) {
79        // Don't follow redirects automatically  return nil to stop
80        completionHandler(nil)
81    }
82}