Skip to content

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 →