diff options
| author | Christian Cleberg <[email protected]> | 2026-03-11 22:01:24 -0500 |
|---|---|---|
| committer | Christian Cleberg <[email protected]> | 2026-03-11 22:01:24 -0500 |
| commit | e11b7d29a6aa9e52f63fcb35ee7ed9127850fffe (patch) | |
| tree | 33252d28c1ef6b4ab4a38c0be793f22129696588 /DomainDig/DomainViewModel.swift | |
| download | domain-dig-e11b7d29a6aa9e52f63fcb35ee7ed9127850fffe.tar.gz domain-dig-e11b7d29a6aa9e52f63fcb35ee7ed9127850fffe.tar.bz2 domain-dig-e11b7d29a6aa9e52f63fcb35ee7ed9127850fffe.zip | |
initial commit
Diffstat (limited to 'DomainDig/DomainViewModel.swift')
| -rw-r--r-- | DomainDig/DomainViewModel.swift | 157 |
1 files changed, 157 insertions, 0 deletions
diff --git a/DomainDig/DomainViewModel.swift b/DomainDig/DomainViewModel.swift new file mode 100644 index 0000000..2965dde --- /dev/null +++ b/DomainDig/DomainViewModel.swift @@ -0,0 +1,157 @@ +import Foundation +import SwiftUI + +@MainActor +@Observable +final class DomainViewModel { + var domain: String = "" + + var dnsSections: [DNSSection] = [] + var dnsLoading = false + var dnsError: String? + + var sslInfo: SSLCertificateInfo? + var sslLoading = false + var sslError: String? + + var hasRun = false + private(set) var searchedDomain: String = "" + + private static let recentSearchesKey = "recentSearches" + private static let maxRecent = 20 + + var recentSearches: [String] = UserDefaults.standard.stringArray(forKey: recentSearchesKey) ?? [] + + var trimmedDomain: String { + domain + .trimmingCharacters(in: .whitespacesAndNewlines) + .replacingOccurrences(of: "https://", with: "") + .replacingOccurrences(of: "http://", with: "") + .components(separatedBy: "/").first ?? "" + } + + func run() { + let target = trimmedDomain + guard !target.isEmpty else { return } + + addRecentSearch(target) + searchedDomain = target + hasRun = true + dnsSections = [] + dnsError = nil + dnsLoading = true + sslInfo = nil + sslError = nil + sslLoading = true + + Task { + await withTaskGroup(of: Void.self) { group in + group.addTask { @MainActor in + await self.runDNS(domain: target) + } + group.addTask { @MainActor in + await self.runSSL(domain: target) + } + } + } + } + + private func runDNS(domain: String) async { + do { + let sections = await DNSLookupService.lookupAll(domain: domain) + dnsSections = sections + } + dnsLoading = false + } + + private func runSSL(domain: String) async { + do { + let info = try await SSLCheckService.check(domain: domain) + sslInfo = info + } catch { + sslError = error.localizedDescription + } + sslLoading = false + } + + /// True when both lookups have finished (regardless of success/failure). + var resultsLoaded: Bool { + hasRun && !dnsLoading && !sslLoading + } + + // MARK: - Export + + func exportText() -> String { + let dateFmt = DateFormatter() + dateFmt.dateFormat = "yyyy-MM-dd HH:mm" + let now = dateFmt.string(from: Date()) + + var lines: [String] = [ + "DomainDig Export", + "Domain: \(searchedDomain)", + "Date: \(now)", + "", + "DNS Records", + "-----------" + ] + + for section in dnsSections { + lines.append(section.recordType.rawValue) + if let error = section.error { + lines.append(" Error: \(error)") + } else if section.records.isEmpty { + lines.append(" No records found") + } else { + for record in section.records { + lines.append(" \(record.value) TTL \(record.ttl)") + } + } + if !section.wildcardRecords.isEmpty { + lines.append("*.\(searchedDomain)") + for record in section.wildcardRecords { + lines.append(" \(record.value) TTL \(record.ttl)") + } + } + } + + if let info = sslInfo { + let certDateFmt = DateFormatter() + certDateFmt.dateStyle = .medium + certDateFmt.timeStyle = .none + + lines.append("") + lines.append("SSL / TLS Certificate") + lines.append("---------------------") + lines.append("Common Name: \(info.commonName)") + lines.append("Issuer: \(info.issuer)") + lines.append("SANs: \(info.subjectAltNames.joined(separator: ", "))") + lines.append("Valid From: \(certDateFmt.string(from: info.validFrom))") + lines.append("Valid Until: \(certDateFmt.string(from: info.validUntil))") + lines.append("Days Until Expiry: \(info.daysUntilExpiry)") + lines.append("Chain Depth: \(info.chainDepth)") + } else if let error = sslError { + lines.append("") + lines.append("SSL / TLS Certificate") + lines.append("---------------------") + lines.append("Error: \(error)") + } + + return lines.joined(separator: "\n") + } + + // MARK: - Recent Searches + + private func addRecentSearch(_ domain: String) { + recentSearches.removeAll { $0.lowercased() == domain.lowercased() } + recentSearches.insert(domain, at: 0) + if recentSearches.count > Self.maxRecent { + recentSearches = Array(recentSearches.prefix(Self.maxRecent)) + } + UserDefaults.standard.set(recentSearches, forKey: Self.recentSearchesKey) + } + + func clearRecentSearches() { + recentSearches.removeAll() + UserDefaults.standard.removeObject(forKey: Self.recentSearchesKey) + } +} |
