1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
|
import Foundation
struct DomainAvailabilityService {
private static let suggestionTLDs = ["net", "io", "dev", "app", "co", "org"]
static func check(domain: String) async -> DomainAvailabilityResult {
let normalizedDomain = normalize(domain)
guard !normalizedDomain.isEmpty else {
return DomainAvailabilityResult(domain: domain, status: .unknown)
}
if await checkViaRDAP(domain: normalizedDomain) == .registered {
debugLog("rdap", domain: normalizedDomain, status: .registered)
return DomainAvailabilityResult(domain: normalizedDomain, status: .registered)
}
let fallbackStatus = await checkViaDNSFallback(domain: normalizedDomain)
let method = fallbackStatus == .registered ? "dns" : "fallback"
debugLog(method, domain: normalizedDomain, status: fallbackStatus)
return DomainAvailabilityResult(domain: normalizedDomain, status: fallbackStatus)
}
static func suggestions(for domain: String, limit: Int = 6) async -> [DomainSuggestionResult] {
let normalizedDomain = normalize(domain)
let candidates = suggestionCandidates(for: normalizedDomain, limit: limit)
guard !candidates.isEmpty else { return [] }
var results: [DomainSuggestionResult] = []
for candidate in candidates {
if Task.isCancelled { break }
let result = await check(domain: candidate)
results.append(DomainSuggestionResult(domain: result.domain, status: result.status))
}
return results
}
private static func checkViaRDAP(domain: String) async -> DomainAvailabilityStatus? {
let status = await RDAPService.registrationStatus(for: domain)
if status == nil {
debugLog("rdap-not-found", domain: domain, details: "Ignoring unavailable response from rdap.org")
}
return status
}
private static func checkViaDNSFallback(domain: String) async -> DomainAvailabilityStatus {
do {
let aRecords = try await DNSLookupService.lookup(domain: domain, recordType: .A)
if !aRecords.isEmpty {
return .registered
}
} catch {
debugLog("dns-a-error", domain: domain, details: error.localizedDescription)
}
do {
let nsRecords = try await DNSLookupService.lookup(domain: domain, recordType: .NS)
if !nsRecords.isEmpty {
return .registered
}
return .unknown
} catch {
debugLog("dns-ns-error", domain: domain, details: error.localizedDescription)
return .unknown
}
}
private static func suggestionCandidates(for domain: String, limit: Int) -> [String] {
let parts = domain.split(separator: ".")
guard parts.count >= 2 else { return [] }
let base = parts.dropLast().joined(separator: ".")
let tld = String(parts.last ?? "")
var candidates: [String] = []
for suggestionTLD in suggestionTLDs where suggestionTLD != tld {
candidates.append("\(base).\(suggestionTLD)")
if candidates.count == limit {
return candidates
}
}
if !base.contains("-"), base.count >= 6, candidates.count < limit {
let midpoint = base.index(base.startIndex, offsetBy: base.count / 2)
let hyphenated = "\(base[..<midpoint])-\(base[midpoint...]).\(tld)"
if hyphenated != domain {
candidates.append(hyphenated)
}
}
return Array(candidates.prefix(limit))
}
private static func normalize(_ domain: String) -> String {
domain
.trimmingCharacters(in: .whitespacesAndNewlines)
.lowercased()
}
private static func debugLog(_ method: String, domain: String, status: DomainAvailabilityStatus) {
#if DEBUG
print("[Availability] \(domain) -> \(status.rawValue) via \(method)")
#endif
}
private static func debugLog(_ method: String, domain: String, details: String) {
#if DEBUG
print("[Availability] \(domain) -> \(method): \(details)")
#endif
}
}
|