summaryrefslogtreecommitdiff
path: root/DomainDig/HTTPHeadersService.swift
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-04-03 17:14:00 -0500
committerChristian Cleberg <[email protected]>2026-04-03 17:14:00 -0500
commit2c38b5edee339e5fe9060a835d9772a71606dc85 (patch)
treeedc223bd3782b638c0b3d3575b6013ea4a5a4425 /DomainDig/HTTPHeadersService.swift
parent8465ef359f88ddef7cad68a0c2e361bb0bfe58ff (diff)
downloaddomain-dig-2c38b5edee339e5fe9060a835d9772a71606dc85.tar.gz
domain-dig-2c38b5edee339e5fe9060a835d9772a71606dc85.tar.bz2
domain-dig-2c38b5edee339e5fe9060a835d9772a71606dc85.zip
Add HTTP header grading and protocol metadatav1.6.0
Diffstat (limited to 'DomainDig/HTTPHeadersService.swift')
-rw-r--r--DomainDig/HTTPHeadersService.swift100
1 files changed, 94 insertions, 6 deletions
diff --git a/DomainDig/HTTPHeadersService.swift b/DomainDig/HTTPHeadersService.swift
index a297165..04efce6 100644
--- a/DomainDig/HTTPHeadersService.swift
+++ b/DomainDig/HTTPHeadersService.swift
@@ -1,22 +1,110 @@
import Foundation
+enum HTTPSecurityGrade: String {
+ case a = "A"
+ case b = "B"
+ case c = "C"
+ case d = "D"
+ case f = "F"
+
+ static func grade(for headers: [HTTPHeader]) -> HTTPSecurityGrade {
+ let presentHeaderNames = Set(headers.map { $0.name.lowercased() })
+ let presentCount = HTTPHeader.securityHeaders.intersection(presentHeaderNames).count
+
+ switch presentCount {
+ case 5:
+ return .a
+ case 4:
+ return .b
+ case 3:
+ return .c
+ case 2:
+ return .d
+ default:
+ return .f
+ }
+ }
+}
+
+struct HTTPHeadersResult {
+ let headers: [HTTPHeader]
+ let statusCode: Int?
+ let responseTimeMs: Int?
+ let httpProtocol: String?
+ let http3Advertised: Bool
+}
+
struct HTTPHeadersService {
- static func fetch(domain: String) async throws -> [HTTPHeader] {
+ static func fetch(domain: String) async throws -> HTTPHeadersResult {
let url = URL(string: "https://\(domain)")!
var request = URLRequest(url: url, timeoutInterval: 10)
request.httpMethod = "HEAD"
+ let metricsDelegate = TaskMetricsDelegate()
+ let startTime = Date()
- let (_, response) = try await URLSession.shared.data(for: request)
+ let (_, response) = try await URLSession.shared.data(for: request, delegate: metricsDelegate)
+ let responseTimeMs = max(0, Int(Date().timeIntervalSince(startTime) * 1000))
guard let httpResponse = response as? HTTPURLResponse else {
throw URLError(.badServerResponse)
}
- return httpResponse.allHeaderFields.compactMap { key, value in
- guard let name = key as? String,
- let val = value as? String else { return nil }
- return HTTPHeader(name: name, value: val)
+ let headers = httpResponse.allHeaderFields.compactMap { entry -> HTTPHeader? in
+ guard let name = entry.key as? String,
+ let value = entry.value as? String else { return nil }
+ return HTTPHeader(name: name, value: value)
}
.sorted { $0.name.lowercased() < $1.name.lowercased() }
+
+ let networkProtocolName = metricsDelegate.metrics?.transactionMetrics
+ .compactMap { $0.networkProtocolName }
+ .last
+ let detectedProtocol = protocolLabel(for: networkProtocolName)
+ let altSvcValue = headerValue(named: "alt-svc", in: httpResponse)
+ let http3Advertised = altSvcValue?.localizedCaseInsensitiveContains("h3") == true
+
+ return HTTPHeadersResult(
+ headers: headers,
+ statusCode: httpResponse.statusCode,
+ responseTimeMs: responseTimeMs,
+ httpProtocol: detectedProtocol,
+ http3Advertised: http3Advertised
+ )
+ }
+
+ private static func protocolLabel(for networkProtocolName: String?) -> String? {
+ guard let networkProtocolName else { return nil }
+
+ let normalized = networkProtocolName.lowercased()
+ if normalized == "h2" {
+ return "HTTP/2"
+ }
+ if normalized == "h3" || normalized.hasPrefix("quic") {
+ return "HTTP/3"
+ }
+ if normalized.hasPrefix("http/") {
+ return normalized.uppercased()
+ }
+
+ return networkProtocolName.uppercased()
+ }
+
+ private static func headerValue(named name: String, in response: HTTPURLResponse) -> String? {
+ response.allHeaderFields.first { key, _ in
+ guard let headerName = key as? String else { return false }
+ return headerName.caseInsensitiveCompare(name) == .orderedSame
+ }?.value as? String
+ }
+}
+
+private final class TaskMetricsDelegate: NSObject, URLSessionTaskDelegate {
+ private(set) var metrics: URLSessionTaskMetrics?
+
+ func urlSession(
+ _ session: URLSession,
+ task: URLSessionTask,
+ didFinishCollecting metrics: URLSessionTaskMetrics
+ ) {
+ self.metrics = metrics
}
}