Skip to content

WHOIS & RDAP Lookup — Kotlin 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 Kotlin implementation — the same logic the interactive tool runs, in a shareable, citable form.

// whois — RDAP input classification + URL building (WHOIS tool, Kotlin port of src/lib/whois.ts; fetch omitted).
data class Classified(val kind: String, val value: String, val tld: String? = null) // kind: domain|ipv4|ipv6|asn|invalid
private val ASN_RE = Regex("""^(?:as)?(\d{1,10})$""", RegexOption.IGNORE_CASE)
private val LABEL_RE = Regex("""^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$""", RegexOption.IGNORE_CASE)
// Strip scheme, userinfo, path, and optional port down to the bare host.
fun stripToHost(raw: String): String {
    var s = raw.trim().replace(Regex("""^[a-z][a-z0-9+.-]*://""", RegexOption.IGNORE_CASE), "")
    s = s.replace(Regex("""^(?:whois|rdap):""", RegexOption.IGNORE_CASE), "")
    if ('@' in s) s = s.substringAfterLast('@')
    s = s.split("/", "?", "#").first()
    val bracketed = Regex("""^\[([0-9a-fA-F:.]+)](?::\d+)?$""").find(s) // [ipv6](:port)?
    if (bracketed != null) return bracketed.groupValues[1]
    return s.replace(Regex("""^([^:\[\]]*\.[^:\[\]]*):\d+$"""), "$1") // dotted host with port
}
fun isValidIpv4(host: String): Boolean { // four decimal octets <= 255, no leading zeros
    val parts = host.split('.')
    return parts.size == 4 && parts.all { p ->
        p.length in 1..3 && p.all { it.isDigit() } && p.toInt() <= 255 && (p == "0" || !p.startsWith("0"))
    }
}
fun isValidIpv6(host: String): Boolean { // hex groups of 1-4, at most one "::" collapse
    if (':' !in host || !host.matches(Regex("[0-9a-fA-F:]+"))) return false
    val halves = host.split("::")
    if (halves.size > 2) return false
    val groups = halves.filter { it.isNotEmpty() }.flatMap { it.split(':') }
    return groups.size <= 8 && groups.all { it.length in 1..4 && it.all { c -> c.isDigit() || c in 'a'..'f' || c in 'A'..'F' } }
}
fun classifyInput(raw: String?): Classified { // ASN (as?\d+) | IPv4 | IPv6 | domain; never throws
    val host = stripToHost(raw ?: "")
    if (host.isEmpty()) return Classified("invalid", "")
    ASN_RE.find(host)?.takeIf { it.groupValues[1].toLong() > 0 }
        ?.let { return Classified("asn", it.groupValues[1].toLong().toString()) }
    if (isValidIpv4(host)) return Classified("ipv4", host)
    if (isValidIpv6(host)) return Classified("ipv6", host.lowercase())
    val norm = host.lowercase().trimEnd('.') // trailing root dot(s)
    val labels = norm.split('.')
    if ('.' !in norm || labels.any { !LABEL_RE.matches(it) }) return Classified("invalid", "")
    return Classified("domain", norm, labels.last())
}
// The RDAP path segment type for a classified input.
fun objectPath(kind: String): String? = when (kind) {
    "domain" -> "domain"; "ipv4", "ipv6" -> "ip"; "asn" -> "autnum"; else -> null
}
// Exactly one slash separates the base and the object path.
fun buildRdapUrl(baseUrl: String, c: Classified): String =
    objectPath(c.kind)?.let { "${baseUrl.trimEnd('/')}/$it/${c.value}" } ?: ""
// Bootstrap (dns.json / asn.json) keys: "lo-hi" ranges or bare numbers.
fun asnInRange(key: String, asn: Long): Boolean {
    val r = key.split('-')
    return if (r.size == 2) asn in r[0].toLong()..r[1].toLong() else asn == r[0].toLong()
}
fun resolveServer(entries: List<Pair<List<String>, String>>, tld: String? = null, asn: Long? = null): String? =
    entries.firstNotNullOfOrNull { (keys, url) ->
        keys.firstOrNull { (tld != null && it.equals(tld, ignoreCase = true)) || (asn != null && asnInRange(it, asn)) }
            ?.let { url }
    }
fun main() {
    val dns = listOf(listOf("org", "com") to "https://rdap.org")
    val asns = listOf(listOf("64496-64511") to "https://rdap.arin.net/registry")
    for (q in listOf("https://CosmoLabs.org/tools#whois", "AS3356", "192.0.2.1", "not_a_domain")) {
        val c = classifyInput(q)
        val base = when (c.kind) {
            "domain" -> resolveServer(dns, tld = c.tld)
            "asn" -> resolveServer(asns, asn = c.value.toLong())
            else -> null
        }
        val path = objectPath(c.kind) ?: "invalid"
        println("%-34s %-8s %s".format(q, path, base?.let { buildRdapUrl(it, 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 →