WHOIS & RDAP Lookup — Swift source
Look up registration data for any domain, IP address, or ASN — straight from your browser to the authoritative RDAP registry over HTTPS. No backend, no database of lookups, fully private.
This is the Swift implementation — the same logic the interactive tool runs, in a shareable, citable form.
// whois — RDAP input classification + URL building (WHOIS tool, Swift port of src/lib/whois.ts; fetch omitted).
import Foundation
extension Character { var isAlnum: Bool { isLetter || isNumber } }
struct Classified { // kind: "domain" | "ipv4" | "ipv6" | "asn" | "invalid"
let kind: String, value: String, tld: String?
static let invalid = Classified(kind: "invalid", value: "", tld: nil)
}
// Strip scheme, userinfo, path, and optional port down to the bare host.
func stripToHost(_ raw: String) -> String {
var s = raw.lowercased().trimmingCharacters(in: .whitespaces)
if let scheme = s.range(of: "://") { s = String(s[scheme.upperBound...]) }
for prefix in ["whois:", "rdap:"] where s.hasPrefix(prefix) { s.removeFirst(prefix.count) }
if let at = s.lastIndex(of: "@") { s = String(s[s.index(after: at)...]) }
for sep in ["/", "?", "#"] { if let cut = s.range(of: sep) { s = String(s[..<cut.lowerBound]) } }
if s.hasPrefix("["), let close = s.firstIndex(of: "]") { // [ipv6](:port)?
return String(s[s.index(after: s.startIndex)..<close])
}
if let colon = s.lastIndex(of: ":"), s.firstIndex(of: ":") == colon { s = String(s[..<colon]) } // host:port
return s
}
func isValidIpv4(_ host: String) -> Bool { // four decimal octets <= 255, no leading zeros
let parts = host.split(separator: ".", omittingEmptySubsequences: false).map(String.init)
return parts.count == 4 && parts.allSatisfy { p in
p.allSatisfy(\.isNumber) && p.count <= 3 && Int(p) != nil && Int(p)! <= 255
&& (p == "0" || !p.hasPrefix("0"))
}
}
func isValidIpv6(_ host: String) -> Bool { // hex groups of 1-4, at most one "::" collapse
guard host.contains(":"), host.allSatisfy({ $0.isHexDigit || $0 == ":" }) else { return false }
let halves = host.components(separatedBy: "::")
guard halves.count <= 2 else { return false }
let groups = halves.filter { !$0.isEmpty }.flatMap { $0.components(separatedBy: ":") }
return groups.count <= 8 && groups.allSatisfy { (1...4).contains($0.count) && $0.allSatisfy(\.isHexDigit) }
}
func classifyInput(_ raw: String) -> Classified { // ASN (as?\d+) | IPv4 | IPv6 | domain
let host = stripToHost(raw)
guard !host.isEmpty else { return .invalid }
let asnDigits = host.hasPrefix("as") ? String(host.dropFirst(2)) : host // AS3356 / 3356
if !asnDigits.isEmpty && asnDigits.count <= 10 && asnDigits.allSatisfy(\.isNumber), let asn = Int(asnDigits), asn > 0 {
return Classified(kind: "asn", value: String(asn), tld: nil)
}
if isValidIpv4(host) { return Classified(kind: "ipv4", value: host, tld: nil) }
if isValidIpv6(host) { return Classified(kind: "ipv6", value: host, tld: nil) }
var norm = host
while norm.hasSuffix(".") { norm.removeLast() } // trailing root dot(s)
guard norm.contains(".") else { return .invalid }
let labels = norm.components(separatedBy: ".")
for label in labels { // a-z0-9 + inner '-', no edge '-'
let ok = !label.isEmpty && label.count <= 63 && label.first!.isAlnum && label.last!.isAlnum
&& !label.dropFirst().dropLast().contains { !($0.isAlnum || $0 == "-") }
if !ok { return .invalid }
}
return Classified(kind: "domain", value: norm, tld: labels.last)
}
func objectPath(_ kind: String) -> String? { // the RDAP path segment type for a classified input
switch kind { case "domain": return "domain"; case "ipv4", "ipv6": return "ip"; case "asn": return "autnum"; default: return nil }
}
func buildRdapUrl(_ baseUrl: String, _ c: Classified) -> String { // one slash before the object path
guard let path = objectPath(c.kind) else { return "" }
var base = baseUrl
while base.hasSuffix("/") { base.removeLast() }
return "\(base)/\(path)/\(c.value)"
}
func asnInRange(_ key: String, _ asn: Int) -> Bool { // asn.json keys: "lo-hi" or bare
let r = key.components(separatedBy: "-")
return r.count == 2 ? (Int(r[0]) ?? 0) <= asn && asn <= (Int(r[1]) ?? 0) : Int(key) == asn
}
// Bootstrap (dns.json / asn.json): exact TLD match, or "lo-hi" ASN ranges.
func resolveServer(_ entries: [(keys: [String], url: String)], tld: String? = nil, asn: Int? = nil) -> String? {
for e in entries {
for k in e.keys {
if let tld, k.lowercased() == tld { return e.url }
if let asn, asnInRange(k, asn) { return e.url }
}
}
return nil
}
let dns = [(keys: ["org", "com"], url: "https://rdap.org")]
let asns = [(keys: ["64496-64511"], url: "https://rdap.arin.net/registry")]
for q in ["https://CosmoLabs.org/tools#whois", "AS3356", "192.0.2.1", "not_a_domain"] {
let c = classifyInput(q)
let base = c.kind == "domain" ? resolveServer(dns, tld: c.tld) : c.kind == "asn" ? resolveServer(asns, asn: Int(c.value)) : nil
let path = objectPath(c.kind) ?? "invalid"
print("\(q.padding(toLength: 34, withPad: " ", startingAt: 0)) \(path.padding(toLength: 8, withPad: " ", startingAt: 0)) \(base.map { buildRdapUrl($0, c) } ?? "")")
}
Also available in 13 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →