krz/domain-dig

an ios app for DNS & SSL analysis

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

v4.5.0: DomainDig/RedirectChainService.swift · raw

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