Skip to content

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

# whois — RDAP input classification + URL building (WHOIS tool, Ruby port of src/lib/whois.ts; fetch omitted).
ASN_RE = /^(?:as)?(\d{1,10})$/i.freeze
LABEL_RE = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i.freeze
module WhoisRdap
  module_function
  # Strip scheme, userinfo, path, and optional port down to the bare host.
  def strip_to_host(raw)
    s = raw.strip.sub(%r{^[a-z][a-z0-9+.\-]*://}i, '').sub(/^(?:whois|rdap):/i, '')
    s = s[(s.rindex('@') + 1)..] if s.include?('@')
    s = s.split(%r{[/?#]}).first.to_s
    return s[/^\[([0-9a-fA-F:.]+)\](?::\d+)?$/, 1] if s.start_with?('[') # [ipv6](:port)?
    s.sub(/^([^:\[\]]*\.[^:\[\]]*):\d+$/, '\1') # dotted host with port
  end
  def ipv4?(host) # four decimal octets <= 255, no leading zeros
    parts = host.split('.')
    parts.size == 4 && parts.all? do |p|
      p.match?(/\A\d{1,3}\z/) && p.to_i <= 255 && (p == '0' || !p.start_with?('0'))
    end
  end
  def ipv6?(host) # hex groups of 1-4, at most one "::" collapse
    return false unless host.include?(':') && host.match?(/\A[0-9a-fA-F:]+\z/)
    halves = host.split('::', -1)
    return false if halves.size > 2
    groups = halves.reject(&:empty?).flat_map { |h| h.split(':') }
    groups.size <= 8 && groups.all? { |g| g.match?(/\A[0-9a-fA-F]{1,4}\z/) }
  end
  # Classify a lookup target: ASN (as?\d+) | IPv4 | IPv6 | domain. Never raises.
  def classify_input(raw)
    host = strip_to_host(raw.to_s)
    return { kind: 'invalid', value: '' } if host.empty?
    if (m = ASN_RE.match(host)) && m[1].to_i.positive?
      return { kind: 'asn', value: m[1].to_i.to_s }
    end
    return { kind: 'ipv4', value: host } if ipv4?(host)
    return { kind: 'ipv6', value: host.downcase } if ipv6?(host)
    norm = host.downcase.sub(/\.+$/, '') # trailing root dot(s)
    labels = norm.split('.')
    unless norm.include?('.') && labels.all? { |l| l.match?(LABEL_RE) }
      return { kind: 'invalid', value: '' }
    end
    { kind: 'domain', value: norm, tld: labels.last }
  end
  # The RDAP path segment type for a classified input.
  def object_path(kind)
    { 'domain' => 'domain', 'ipv4' => 'ip', 'ipv6' => 'ip', 'asn' => 'autnum' }[kind]
  end
  # Exactly one slash separates the base and the object path.
  def build_rdap_url(base_url, classified)
    path = object_path(classified[:kind])
    path ? "#{base_url.sub(%r{/+$}, '')}/#{path}/#{classified[:value]}" : ''
  end
  # Bootstrap (dns.json / asn.json) keys: "lo-hi" ranges or bare numbers.
  def asn_in_range(key, asn)
    lo, hi = key.split('-')
    hi ? asn.between?(lo.to_i, hi.to_i) : asn == lo.to_i
  end
  def resolve_server(entries, tld: nil, asn: nil)
    entries.each do |keys, url|
      hit = keys.any? { |k| (tld && k.casecmp?(tld)) || (asn && asn_in_range(k, asn)) }
      return url if hit
    end
    nil
  end
end
if $PROGRAM_NAME == __FILE__
  dns = [[%w[org com], 'https://rdap.org']]
  asns = [[['64496-64511'], 'https://rdap.arin.net/registry']]
  ['https://CosmoLabs.org/tools#whois', 'AS3356', '192.0.2.1', 'not_a_domain'].each do |q|
    c = WhoisRdap.classify_input(q)
    base = if c[:kind] == 'domain'
             WhoisRdap.resolve_server(dns, tld: c[:tld])
           elsif c[:kind] == 'asn'
             WhoisRdap.resolve_server(asns, asn: c[:value].to_i)
           end
    path = WhoisRdap.object_path(c[:kind]) || 'invalid'
    puts format('%-34s %-8s %s', q, path, base ? WhoisRdap.build_rdap_url(base, c) : '')
  end
end

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 →