krz/domain-dig
an ios app for DNS & SSL analysis
clone: git clone https://gitbay.org/krz/domain-dig.git
v2.0.0: DomainDig/Models.swift · raw
1import Foundation
2
3enum ServiceResult<Value> {
4 case success(Value)
5 case empty(String)
6 case error(String)
7}
8
9enum DomainAvailabilityStatus: String, Codable {
10 case available
11 case registered
12 case unknown
13}
14
15struct DomainAvailabilityResult: Codable {
16 let domain: String
17 let status: DomainAvailabilityStatus
18}
19
20struct DomainSuggestionResult: Identifiable, Codable {
21 let id: UUID
22 let domain: String
23 let status: DomainAvailabilityStatus
24
25 init(id: UUID = UUID(), domain: String, status: DomainAvailabilityStatus) {
26 self.id = id
27 self.domain = domain
28 self.status = status
29 }
30}
31
32struct WatchedDomain: Codable, Identifiable {
33 let id: UUID
34 let domain: String
35 let createdAt: Date
36 var lastKnownAvailability: DomainAvailabilityStatus?
37
38 init(
39 id: UUID = UUID(),
40 domain: String,
41 createdAt: Date = Date(),
42 lastKnownAvailability: DomainAvailabilityStatus? = nil
43 ) {
44 self.id = id
45 self.domain = domain
46 self.createdAt = createdAt
47 self.lastKnownAvailability = lastKnownAvailability
48 }
49}
50
51struct DomainChangeSummary: Codable, Equatable {
52 let hasChanges: Bool
53 let changedSections: [String]
54 let generatedAt: Date
55}
56
57enum PremiumCapability: String, Codable {
58 case unlimitedTrackedDomains
59 case automatedMonitoring
60 case pushAlerts
61 case batchTracking
62 case advancedExports
63}
64
65struct TrackedDomain: Codable, Identifiable, Equatable {
66 let id: UUID
67 var domain: String
68 var createdAt: Date
69 var updatedAt: Date
70 var note: String?
71 var isPinned: Bool
72 var lastKnownAvailability: DomainAvailabilityStatus?
73 var lastSnapshotID: UUID?
74 var lastChangeSummary: DomainChangeSummary?
75
76 init(
77 id: UUID = UUID(),
78 domain: String,
79 createdAt: Date = Date(),
80 updatedAt: Date = Date(),
81 note: String? = nil,
82 isPinned: Bool = false,
83 lastKnownAvailability: DomainAvailabilityStatus? = nil,
84 lastSnapshotID: UUID? = nil,
85 lastChangeSummary: DomainChangeSummary? = nil
86 ) {
87 self.id = id
88 self.domain = domain
89 self.createdAt = createdAt
90 self.updatedAt = updatedAt
91 self.note = note
92 self.isPinned = isPinned
93 self.lastKnownAvailability = lastKnownAvailability
94 self.lastSnapshotID = lastSnapshotID
95 self.lastChangeSummary = lastChangeSummary
96 }
97}
98
99// MARK: - DNS Models
100
101enum DNSRecordType: String, CaseIterable, Codable {
102 case A
103 case AAAA
104 case MX
105 case NS
106 case TXT
107 case CNAME
108 case SOA
109 case SRV
110 case CAA
111 case DS
112 case PTR
113
114 var queryType: Int {
115 switch self {
116 case .A: return 1
117 case .AAAA: return 28
118 case .MX: return 15
119 case .NS: return 2
120 case .TXT: return 16
121 case .CNAME: return 5
122 case .SOA: return 6
123 case .SRV: return 33
124 case .CAA: return 257
125 case .DS: return 43
126 case .PTR: return 12
127 }
128 }
129
130 var usesRawDataValue: Bool {
131 switch self {
132 case .TXT, .SOA, .DS:
133 return true
134 default:
135 return false
136 }
137 }
138}
139
140struct DNSRecord: Identifiable, Codable {
141 var id = UUID()
142 let value: String
143 let ttl: Int
144}
145
146struct DNSSection: Identifiable, Codable {
147 var id = UUID()
148 let recordType: DNSRecordType
149 var records: [DNSRecord]
150 var wildcardRecords: [DNSRecord] = []
151 var dnssecSigned: Bool?
152 var error: String?
153}
154
155// MARK: - SSL Models
156
157struct SSLCertificateInfo: Codable {
158 struct CertChainEntry: Codable {
159 let subject: String
160 let issuer: String
161 }
162
163 let commonName: String
164 let subjectAltNames: [String]
165 let issuer: String
166 let validFrom: Date
167 let validUntil: Date
168 let daysUntilExpiry: Int
169 let chainDepth: Int
170 let tlsVersion: String?
171 let cipherSuite: String?
172 let chain: [CertChainEntry]
173
174 init(
175 commonName: String,
176 subjectAltNames: [String],
177 issuer: String,
178 validFrom: Date,
179 validUntil: Date,
180 daysUntilExpiry: Int,
181 chainDepth: Int,
182 tlsVersion: String? = nil,
183 cipherSuite: String? = nil,
184 chain: [CertChainEntry] = []
185 ) {
186 self.commonName = commonName
187 self.subjectAltNames = subjectAltNames
188 self.issuer = issuer
189 self.validFrom = validFrom
190 self.validUntil = validUntil
191 self.daysUntilExpiry = daysUntilExpiry
192 self.chainDepth = chainDepth
193 self.tlsVersion = tlsVersion
194 self.cipherSuite = cipherSuite
195 self.chain = chain
196 }
197
198 init(from decoder: Decoder) throws {
199 let container = try decoder.container(keyedBy: CodingKeys.self)
200 commonName = try container.decode(String.self, forKey: .commonName)
201 subjectAltNames = try container.decode([String].self, forKey: .subjectAltNames)
202 issuer = try container.decode(String.self, forKey: .issuer)
203 validFrom = try container.decode(Date.self, forKey: .validFrom)
204 validUntil = try container.decode(Date.self, forKey: .validUntil)
205 daysUntilExpiry = try container.decode(Int.self, forKey: .daysUntilExpiry)
206 chainDepth = try container.decode(Int.self, forKey: .chainDepth)
207 tlsVersion = try container.decodeIfPresent(String.self, forKey: .tlsVersion)
208 cipherSuite = try container.decodeIfPresent(String.self, forKey: .cipherSuite)
209 chain = try container.decodeIfPresent([CertChainEntry].self, forKey: .chain) ?? []
210 }
211}
212
213// MARK: - HTTP Headers Models
214
215struct HTTPHeader: Identifiable, Codable {
216 var id = UUID()
217 let name: String
218 let value: String
219
220 static let securityHeaders: Set<String> = [
221 "strict-transport-security",
222 "x-frame-options",
223 "x-content-type-options",
224 "content-security-policy",
225 "referrer-policy"
226 ]
227
228 var isSecurityHeader: Bool {
229 Self.securityHeaders.contains(name.lowercased())
230 }
231}
232
233// MARK: - Reachability Models
234
235struct PortReachability: Identifiable, Codable {
236 var id = UUID()
237 let port: UInt16
238 let reachable: Bool
239 let latencyMs: Int?
240}
241
242// MARK: - IP Geolocation Models
243
244struct IPGeolocation: Codable {
245 let ip: String
246 let city: String?
247 let region: String?
248 let country_name: String?
249 let org: String?
250 let latitude: Double?
251 let longitude: Double?
252}
253
254// MARK: - Email Security Models
255
256struct EmailSecurityResult: Codable {
257 let spf: EmailSecurityRecord
258 let dmarc: EmailSecurityRecord
259 let dkim: EmailSecurityRecord
260 let bimi: EmailSecurityRecord
261 let mtaSts: MTASTSResult?
262
263 init(
264 spf: EmailSecurityRecord,
265 dmarc: EmailSecurityRecord,
266 dkim: EmailSecurityRecord,
267 bimi: EmailSecurityRecord = EmailSecurityRecord(found: false, value: nil),
268 mtaSts: MTASTSResult? = nil
269 ) {
270 self.spf = spf
271 self.dmarc = dmarc
272 self.dkim = dkim
273 self.bimi = bimi
274 self.mtaSts = mtaSts
275 }
276
277 init(from decoder: Decoder) throws {
278 let container = try decoder.container(keyedBy: CodingKeys.self)
279 spf = try container.decode(EmailSecurityRecord.self, forKey: .spf)
280 dmarc = try container.decode(EmailSecurityRecord.self, forKey: .dmarc)
281 dkim = try container.decode(EmailSecurityRecord.self, forKey: .dkim)
282 bimi = try container.decodeIfPresent(EmailSecurityRecord.self, forKey: .bimi)
283 ?? EmailSecurityRecord(found: false, value: nil)
284 mtaSts = try container.decodeIfPresent(MTASTSResult.self, forKey: .mtaSts)
285 }
286}
287
288struct EmailSecurityRecord: Codable {
289 let found: Bool
290 let value: String?
291 let matchedSelector: String?
292
293 init(found: Bool, value: String?, matchedSelector: String? = nil) {
294 self.found = found
295 self.value = value
296 self.matchedSelector = matchedSelector
297 }
298
299 init(from decoder: Decoder) throws {
300 let container = try decoder.container(keyedBy: CodingKeys.self)
301 found = try container.decode(Bool.self, forKey: .found)
302 value = try container.decodeIfPresent(String.self, forKey: .value)
303 matchedSelector = try container.decodeIfPresent(String.self, forKey: .matchedSelector)
304 }
305}
306
307struct MTASTSResult: Codable {
308 let txtFound: Bool
309 let policyMode: String?
310}
311
312// MARK: - Redirect Chain Models
313
314struct RedirectHop: Identifiable, Codable {
315 var id = UUID()
316 let stepNumber: Int
317 let statusCode: Int
318 let url: String
319 let isFinal: Bool
320}
321
322// MARK: - Port Scan Models
323
324enum PortScanKind: String, Codable {
325 case standard
326 case custom
327}
328
329struct PortScanResult: Identifiable, Codable {
330 var id = UUID()
331 let port: UInt16
332 let service: String
333 let open: Bool
334 var banner: String?
335 let kind: PortScanKind
336 let durationMs: Int?
337
338 nonisolated init(
339 port: UInt16,
340 service: String,
341 open: Bool,
342 banner: String? = nil,
343 kind: PortScanKind = .standard,
344 durationMs: Int? = nil
345 ) {
346 self.port = port
347 self.service = service
348 self.open = open
349 self.banner = banner
350 self.kind = kind
351 self.durationMs = durationMs
352 }
353
354 init(from decoder: Decoder) throws {
355 let container = try decoder.container(keyedBy: CodingKeys.self)
356 id = try container.decodeIfPresent(UUID.self, forKey: .id) ?? UUID()
357 port = try container.decode(UInt16.self, forKey: .port)
358 service = try container.decode(String.self, forKey: .service)
359 open = try container.decode(Bool.self, forKey: .open)
360 banner = try container.decodeIfPresent(String.self, forKey: .banner)
361 kind = try container.decodeIfPresent(PortScanKind.self, forKey: .kind) ?? .standard
362 durationMs = try container.decodeIfPresent(Int.self, forKey: .durationMs)
363 }
364}
365
366// MARK: - History Models
367
368struct HistoryEntry: Identifiable, Codable {
369 var id = UUID()
370 let domain: String
371 let timestamp: Date
372 var trackedDomainID: UUID?
373 let dnsSections: [DNSSection]
374 let sslInfo: SSLCertificateInfo?
375 let httpHeaders: [HTTPHeader]
376 let reachabilityResults: [PortReachability]
377 let ipGeolocation: IPGeolocation?
378 var emailSecurity: EmailSecurityResult?
379 var mtaSts: MTASTSResult?
380 var ptrRecord: String?
381 var redirectChain: [RedirectHop]
382 var portScanResults: [PortScanResult]
383 var hstsPreloaded: Bool?
384 var availabilityResult: DomainAvailabilityResult?
385 var suggestions: [DomainSuggestionResult]
386 var resolverDisplayName: String
387 var resolverURLString: String
388 var totalLookupDurationMs: Int?
389 var primaryIP: String?
390 var finalRedirectURL: String?
391 var tlsStatusSummary: String?
392 var emailSecuritySummary: String?
393 var httpGradeSummary: String?
394 var changeSummary: DomainChangeSummary?
395 var sslError: String?
396 var httpHeadersError: String?
397 var reachabilityError: String?
398 var ipGeolocationError: String?
399 var emailSecurityError: String?
400 var ptrError: String?
401 var redirectChainError: String?
402 var portScanError: String?
403
404 init(domain: String, timestamp: Date, trackedDomainID: UUID? = nil, dnsSections: [DNSSection],
405 sslInfo: SSLCertificateInfo?, httpHeaders: [HTTPHeader],
406 reachabilityResults: [PortReachability], ipGeolocation: IPGeolocation?,
407 emailSecurity: EmailSecurityResult? = nil, mtaSts: MTASTSResult? = nil, ptrRecord: String? = nil,
408 redirectChain: [RedirectHop] = [], portScanResults: [PortScanResult] = [],
409 hstsPreloaded: Bool? = nil, availabilityResult: DomainAvailabilityResult? = nil,
410 suggestions: [DomainSuggestionResult] = [], resolverDisplayName: String, resolverURLString: String,
411 totalLookupDurationMs: Int? = nil, primaryIP: String? = nil, finalRedirectURL: String? = nil,
412 tlsStatusSummary: String? = nil, emailSecuritySummary: String? = nil, httpGradeSummary: String? = nil,
413 changeSummary: DomainChangeSummary? = nil, sslError: String? = nil, httpHeadersError: String? = nil,
414 reachabilityError: String? = nil, ipGeolocationError: String? = nil,
415 emailSecurityError: String? = nil, ptrError: String? = nil,
416 redirectChainError: String? = nil, portScanError: String? = nil) {
417 self.domain = domain
418 self.timestamp = timestamp
419 self.trackedDomainID = trackedDomainID
420 self.dnsSections = dnsSections
421 self.sslInfo = sslInfo
422 self.httpHeaders = httpHeaders
423 self.reachabilityResults = reachabilityResults
424 self.ipGeolocation = ipGeolocation
425 self.emailSecurity = emailSecurity
426 self.mtaSts = mtaSts ?? emailSecurity?.mtaSts
427 self.ptrRecord = ptrRecord
428 self.redirectChain = redirectChain
429 self.portScanResults = portScanResults
430 self.hstsPreloaded = hstsPreloaded
431 self.availabilityResult = availabilityResult
432 self.suggestions = suggestions
433 self.resolverDisplayName = resolverDisplayName
434 self.resolverURLString = resolverURLString
435 self.totalLookupDurationMs = totalLookupDurationMs
436 self.primaryIP = primaryIP
437 self.finalRedirectURL = finalRedirectURL
438 self.tlsStatusSummary = tlsStatusSummary
439 self.emailSecuritySummary = emailSecuritySummary
440 self.httpGradeSummary = httpGradeSummary
441 self.changeSummary = changeSummary
442 self.sslError = sslError
443 self.httpHeadersError = httpHeadersError
444 self.reachabilityError = reachabilityError
445 self.ipGeolocationError = ipGeolocationError
446 self.emailSecurityError = emailSecurityError
447 self.ptrError = ptrError
448 self.redirectChainError = redirectChainError
449 self.portScanError = portScanError
450 }
451
452 init(from decoder: Decoder) throws {
453 let container = try decoder.container(keyedBy: CodingKeys.self)
454 id = try container.decodeIfPresent(UUID.self, forKey: .id) ?? UUID()
455 domain = try container.decode(String.self, forKey: .domain)
456 timestamp = try container.decode(Date.self, forKey: .timestamp)
457 trackedDomainID = try container.decodeIfPresent(UUID.self, forKey: .trackedDomainID)
458 dnsSections = try container.decode([DNSSection].self, forKey: .dnsSections)
459 sslInfo = try container.decodeIfPresent(SSLCertificateInfo.self, forKey: .sslInfo)
460 httpHeaders = try container.decode([HTTPHeader].self, forKey: .httpHeaders)
461 reachabilityResults = try container.decode([PortReachability].self, forKey: .reachabilityResults)
462 ipGeolocation = try container.decodeIfPresent(IPGeolocation.self, forKey: .ipGeolocation)
463 emailSecurity = try container.decodeIfPresent(EmailSecurityResult.self, forKey: .emailSecurity)
464 mtaSts = try container.decodeIfPresent(MTASTSResult.self, forKey: .mtaSts) ?? emailSecurity?.mtaSts
465 ptrRecord = try container.decodeIfPresent(String.self, forKey: .ptrRecord)
466 redirectChain = try container.decodeIfPresent([RedirectHop].self, forKey: .redirectChain) ?? []
467 portScanResults = try container.decodeIfPresent([PortScanResult].self, forKey: .portScanResults) ?? []
468 hstsPreloaded = try container.decodeIfPresent(Bool.self, forKey: .hstsPreloaded)
469 availabilityResult = try container.decodeIfPresent(DomainAvailabilityResult.self, forKey: .availabilityResult)
470 suggestions = try container.decodeIfPresent([DomainSuggestionResult].self, forKey: .suggestions) ?? []
471 resolverDisplayName = try container.decodeIfPresent(String.self, forKey: .resolverDisplayName) ?? "Cloudflare"
472 resolverURLString = try container.decodeIfPresent(String.self, forKey: .resolverURLString) ?? DNSResolverOption.defaultURLString
473 totalLookupDurationMs = try container.decodeIfPresent(Int.self, forKey: .totalLookupDurationMs)
474 primaryIP = try container.decodeIfPresent(String.self, forKey: .primaryIP)
475 finalRedirectURL = try container.decodeIfPresent(String.self, forKey: .finalRedirectURL)
476 tlsStatusSummary = try container.decodeIfPresent(String.self, forKey: .tlsStatusSummary)
477 emailSecuritySummary = try container.decodeIfPresent(String.self, forKey: .emailSecuritySummary)
478 httpGradeSummary = try container.decodeIfPresent(String.self, forKey: .httpGradeSummary)
479 changeSummary = try container.decodeIfPresent(DomainChangeSummary.self, forKey: .changeSummary)
480 sslError = try container.decodeIfPresent(String.self, forKey: .sslError)
481 httpHeadersError = try container.decodeIfPresent(String.self, forKey: .httpHeadersError)
482 reachabilityError = try container.decodeIfPresent(String.self, forKey: .reachabilityError)
483 ipGeolocationError = try container.decodeIfPresent(String.self, forKey: .ipGeolocationError)
484 emailSecurityError = try container.decodeIfPresent(String.self, forKey: .emailSecurityError)
485 ptrError = try container.decodeIfPresent(String.self, forKey: .ptrError)
486 redirectChainError = try container.decodeIfPresent(String.self, forKey: .redirectChainError)
487 portScanError = try container.decodeIfPresent(String.self, forKey: .portScanError)
488 }
489}
490
491// MARK: - Cloudflare DNS-over-HTTPS Response
492
493struct CloudflareDNSResponse: Decodable {
494 let Status: Int
495 let AD: Bool?
496 let Answer: [CloudflareDNSAnswer]?
497
498 struct CloudflareDNSAnswer: Decodable {
499 let name: String
500 let type: Int
501 let TTL: Int
502 let data: String
503 }
504}