krz/domain-dig

an ios app for DNS & SSL analysis

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

v1.7.0: DomainDig/HTTPHeadersService.swift · raw

  1import Foundation
  2
  3enum HTTPSecurityGrade: String {
  4    case a = "A"
  5    case b = "B"
  6    case c = "C"
  7    case d = "D"
  8    case f = "F"
  9
 10    static func grade(for headers: [HTTPHeader]) -> HTTPSecurityGrade {
 11        let presentHeaderNames = Set(headers.map { $0.name.lowercased() })
 12        let presentCount = HTTPHeader.securityHeaders.intersection(presentHeaderNames).count
 13
 14        switch presentCount {
 15        case 5:
 16            return .a
 17        case 4:
 18            return .b
 19        case 3:
 20            return .c
 21        case 2:
 22            return .d
 23        default:
 24            return .f
 25        }
 26    }
 27}
 28
 29struct HTTPHeadersResult {
 30    let headers: [HTTPHeader]
 31    let statusCode: Int?
 32    let responseTimeMs: Int?
 33    let httpProtocol: String?
 34    let http3Advertised: Bool
 35}
 36
 37struct HTTPHeadersService {
 38    static func fetch(domain: String) async throws -> HTTPHeadersResult {
 39        let url = URL(string: "https://\(domain)")!
 40        var request = URLRequest(url: url, timeoutInterval: 10)
 41        request.httpMethod = "HEAD"
 42        let metricsDelegate = TaskMetricsDelegate()
 43        let startTime = Date()
 44
 45        let (_, response) = try await URLSession.shared.data(for: request, delegate: metricsDelegate)
 46        let responseTimeMs = max(0, Int(Date().timeIntervalSince(startTime) * 1000))
 47
 48        guard let httpResponse = response as? HTTPURLResponse else {
 49            throw URLError(.badServerResponse)
 50        }
 51
 52        let headers = httpResponse.allHeaderFields.compactMap { entry -> HTTPHeader? in
 53            guard let name = entry.key as? String,
 54                  let value = entry.value as? String else { return nil }
 55            return HTTPHeader(name: name, value: value)
 56        }
 57        .sorted { $0.name.lowercased() < $1.name.lowercased() }
 58
 59        let networkProtocolName = metricsDelegate.metrics?.transactionMetrics
 60            .compactMap { $0.networkProtocolName }
 61            .last
 62        let detectedProtocol = protocolLabel(for: networkProtocolName)
 63        let altSvcValue = headerValue(named: "alt-svc", in: httpResponse)
 64        let http3Advertised = altSvcValue?.localizedCaseInsensitiveContains("h3") == true
 65
 66        return HTTPHeadersResult(
 67            headers: headers,
 68            statusCode: httpResponse.statusCode,
 69            responseTimeMs: responseTimeMs,
 70            httpProtocol: detectedProtocol,
 71            http3Advertised: http3Advertised
 72        )
 73    }
 74
 75    private static func protocolLabel(for networkProtocolName: String?) -> String? {
 76        guard let networkProtocolName else { return nil }
 77
 78        let normalized = networkProtocolName.lowercased()
 79        if normalized == "h2" {
 80            return "HTTP/2"
 81        }
 82        if normalized == "h3" || normalized.hasPrefix("quic") {
 83            return "HTTP/3"
 84        }
 85        if normalized.hasPrefix("http/") {
 86            return normalized.uppercased()
 87        }
 88
 89        return networkProtocolName.uppercased()
 90    }
 91
 92    private static func headerValue(named name: String, in response: HTTPURLResponse) -> String? {
 93        response.allHeaderFields.first { key, _ in
 94            guard let headerName = key as? String else { return false }
 95            return headerName.caseInsensitiveCompare(name) == .orderedSame
 96        }?.value as? String
 97    }
 98}
 99
100private final class TaskMetricsDelegate: NSObject, URLSessionTaskDelegate {
101    private(set) var metrics: URLSessionTaskMetrics?
102
103    func urlSession(
104        _ session: URLSession,
105        task: URLSessionTask,
106        didFinishCollecting metrics: URLSessionTaskMetrics
107    ) {
108        self.metrics = metrics
109    }
110}