krz/domain-dig
an ios app for DNS & SSL analysis
clone: git clone https://gitbay.org/krz/domain-dig.git
v1.7.1: 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 _ _: URLSession,
105 task _: URLSessionTask,
106 didFinishCollecting metrics: URLSessionTaskMetrics
107 ) {
108 self.metrics = metrics
109 }
110}