krz/domain-dig
an ios app for DNS & SSL analysis
clone: git clone https://gitbay.org/krz/domain-dig.git
v5.0.3: 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}