diff options
| author | Christian Cleberg <[email protected]> | 2026-04-03 17:14:00 -0500 |
|---|---|---|
| committer | Christian Cleberg <[email protected]> | 2026-04-03 17:14:00 -0500 |
| commit | 2c38b5edee339e5fe9060a835d9772a71606dc85 (patch) | |
| tree | edc223bd3782b638c0b3d3575b6013ea4a5a4425 /DomainDig/HTTPHeadersService.swift | |
| parent | 8465ef359f88ddef7cad68a0c2e361bb0bfe58ff (diff) | |
| download | domain-dig-1.6.0.tar.gz domain-dig-1.6.0.tar.bz2 domain-dig-1.6.0.zip | |
Add HTTP header grading and protocol metadatav1.6.0
Diffstat (limited to 'DomainDig/HTTPHeadersService.swift')
| -rw-r--r-- | DomainDig/HTTPHeadersService.swift | 100 |
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 } } |
