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}