summaryrefslogtreecommitdiff
path: root/DomainDig/RedirectChainService.swift
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-03-18 11:59:38 -0500
committerChristian Cleberg <[email protected]>2026-03-18 11:59:38 -0500
commit376e7b55b4a0c4f54faa0c2828dea4fd17dff288 (patch)
tree540d11ad423f14975553f2490617e05aea61ae2c /DomainDig/RedirectChainService.swift
parente11b7d29a6aa9e52f63fcb35ee7ed9127850fffe (diff)
downloaddomain-dig-376e7b55b4a0c4f54faa0c2828dea4fd17dff288.tar.gz
domain-dig-376e7b55b4a0c4f54faa0c2828dea4fd17dff288.tar.bz2
domain-dig-376e7b55b4a0c4f54faa0c2828dea4fd17dff288.zip
realigning commits
Diffstat (limited to 'DomainDig/RedirectChainService.swift')
-rw-r--r--DomainDig/RedirectChainService.swift82
1 files changed, 82 insertions, 0 deletions
diff --git a/DomainDig/RedirectChainService.swift b/DomainDig/RedirectChainService.swift
new file mode 100644
index 0000000..7a23a5e
--- /dev/null
+++ b/DomainDig/RedirectChainService.swift
@@ -0,0 +1,82 @@
+import Foundation
+
+struct RedirectChainService {
+ static func trace(domain: String) async throws -> [RedirectHop] {
+ // Try HTTPS first (avoids ATS issues), fall back to HTTP if it fails entirely
+ do {
+ return try await followChain(startingURL: URL(string: "https://\(domain)")!)
+ } catch {
+ return try await followChain(startingURL: URL(string: "http://\(domain)")!)
+ }
+ }
+
+ 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(
+ _ session: URLSession,
+ task: URLSessionTask,
+ willPerformHTTPRedirection response: HTTPURLResponse,
+ newRequest request: URLRequest,
+ completionHandler: @escaping (URLRequest?) -> Void
+ ) {
+ // Don't follow redirects automatically — return nil to stop
+ completionHandler(nil)
+ }
+}