summaryrefslogtreecommitdiff
path: root/DomainDig/RedirectChainService.swift
blob: d55a4aacb259a8abbd583279ade66d2f8fc7c030 (plain) (blame)
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
import Foundation

struct RedirectChainService {
    static func trace(domain: String) async -> ServiceResult<[RedirectHop]> {
        do {
            let hops = try await followChain(startingURL: URL(string: "https://\(domain)")!)
            return hops.isEmpty ? .empty("No redirect data available") : .success(hops)
        } catch {
            do {
                let hops = try await followChain(startingURL: URL(string: "http://\(domain)")!)
                return hops.isEmpty ? .empty("No redirect data available") : .success(hops)
            } catch {
                return .error(error.localizedDescription)
            }
        }
    }

    private static func followChain(startingURL: URL) async throws -> [RedirectHop] {
        let delegate = NoRedirectDelegate()
        let session = URLSession(
            configuration: .ephemeral,
            delegate: delegate,
            delegateQueue: nil
        )
        defer { session.invalidateAndCancel() }

        var hops: [RedirectHop] = []
        var currentURL = startingURL
        let maxRedirects = 10

        for step in 1...maxRedirects + 1 {
            var request = URLRequest(url: currentURL, timeoutInterval: 10)
            request.httpMethod = "GET"

            let (_, response) = try await session.data(for: request)

            guard let httpResponse = response as? HTTPURLResponse else {
                throw URLError(.badServerResponse)
            }

            let statusCode = httpResponse.statusCode
            let isRedirect = (300...399).contains(statusCode)

            if isRedirect, let location = httpResponse.value(forHTTPHeaderField: "Location") {
                hops.append(RedirectHop(
                    stepNumber: step,
                    statusCode: statusCode,
                    url: currentURL.absoluteString,
                    isFinal: false
                ))

                // Resolve relative redirects
                if let nextURL = URL(string: location, relativeTo: currentURL)?.absoluteURL {
                    currentURL = nextURL
                } else {
                    break
                }

                if step > maxRedirects { break }
            } else {
                // Non-redirect — this is the final destination
                hops.append(RedirectHop(
                    stepNumber: step,
                    statusCode: statusCode,
                    url: currentURL.absoluteString,
                    isFinal: true
                ))
                break
            }
        }

        return hops
    }
}

private final class NoRedirectDelegate: NSObject, URLSessionTaskDelegate, @unchecked Sendable {
    func urlSession(
        _ _: URLSession,
        task _: URLSessionTask,
        willPerformHTTPRedirection _: HTTPURLResponse,
        newRequest _: URLRequest,
        completionHandler: @escaping (URLRequest?) -> Void
    ) {
        // Don't follow redirects automatically — return nil to stop
        completionHandler(nil)
    }
}