summaryrefslogtreecommitdiff
path: root/DomainDig/DomainViewModel.swift
diff options
context:
space:
mode:
Diffstat (limited to 'DomainDig/DomainViewModel.swift')
-rw-r--r--DomainDig/DomainViewModel.swift157
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)
+ }
+}