Skip to content

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

// whois — RDAP input classification + URL building (WHOIS tool, Zig port of src/lib/whois.ts; fetch omitted).
const std = @import("std");
const Kind = enum { invalid, domain, ipv4, ipv6, asn };
const Classified = struct { kind: Kind = .invalid, value: []const u8 = "", tld: []const u8 = "" };
fn isDigit(c: u8) bool { return c >= '0' and c <= '9'; }
fn isHex(c: u8) bool { return isDigit(c) or (c >= 'a' and c <= 'f') or (c >= 'A' and c <= 'F'); }
fn isAlnum(c: u8) bool { return isDigit(c) or (c >= 'a' and c <= 'z') or (c >= 'A' and c <= 'Z'); }
fn labelOk(s: []const u8) bool { // a-z0-9 + inner '-', no edge '-'
    if (s.len == 0 or s.len > 63 or !isAlnum(s[0]) or !isAlnum(s[s.len - 1])) return false;
    for (s[1 .. s.len - 1]) |c| if (!isAlnum(c) and c != '-') return false;
    return true;
}
fn v4(s: []const u8) bool { // four decimal octets <= 255, no leading zeros
    var it = std.mem.splitScalar(u8, s, '.');
    var n: usize = 0;
    while (it.next()) |o| {
        n += 1;
        if (n > 4 or o.len < 1 or o.len > 3 or (o[0] == '0' and o.len > 1)) return false;
        var v: u32 = 0;
        for (o) |c| {
            if (!isDigit(c)) return false;
            v = v * 10 + (c - '0');
        }
        if (v > 255) return false;
    }
    return n == 4;
}
fn v6(s: []const u8) bool { // hex groups of 1-4, at most one "::" collapse
    const dd = std.mem.indexOf(u8, s, "::");
    if (std.mem.indexOfScalar(u8, s, ':') == null) return false;
    if (dd != null and std.mem.indexOfPos(u8, s, dd.? + 1, "::") != null) return false;
    var groups: usize = 0;
    var i: usize = 0;
    while (i < s.len) {
        if (s[i] == ':') { i += 1; continue; }
        var len: usize = 0;
        while (i + len < s.len and isHex(s[i + len])) len += 1;
        if (len < 1 or len > 4) return false;
        groups += 1;
        i += len;
    }
    return groups <= 8 and (dd != null or groups == 8);
}
fn stripToHost(raw: []const u8, out: []u8) []const u8 { // scheme/userinfo/path/port -> bare host
    var s: []const u8 = raw;
    if (std.mem.indexOf(u8, s, "://")) |i| s = s[i + 3 ..];
    if (std.mem.startsWith(u8, s, "whois:")) s = s[6..];
    if (std.mem.startsWith(u8, s, "rdap:")) s = s[5..];
    if (std.mem.lastIndexOfScalar(u8, s, '@')) |i| s = s[i + 1 ..];
    for (s, 0..) |c, i| if (c == '/' or c == '?' or c == '#') { // cut at the first path byte
        s = s[0..i];
        break;
    };
    if (s.len > 0 and s[0] == '[') { // [ipv6](:port)?
        if (std.mem.indexOfScalar(u8, s, ']')) |close| s = s[1..close];
    } else if (std.mem.lastIndexOfScalar(u8, s, ':')) |colon| {
        if (std.mem.indexOfScalar(u8, s, ':') == colon) s = s[0..colon]; // dotted host with port
    }
    for (out[0..s.len], s) |*o, c| o.* = std.ascii.toLower(c);
    return out[0..s.len];
}
fn classify(raw: []const u8, buf: []u8) Classified { // ASN (as?\d+) | IPv4 | IPv6 | domain
    const host = stripToHost(raw, buf);
    if (host.len == 0) return .{};
    const a = if (std.mem.startsWith(u8, host, "as")) host[2..] else host; // AS3356 / 3356
    if (a.len > 0 and a.len <= 10 and std.mem.all(u8, a, isDigit)) {
        var digits = a;
        while (digits.len > 1 and digits[0] == '0') digits = digits[1..]; // strip leading zeros
        if (digits[0] != '0') return .{ .kind = .asn, .value = digits };
    }
    if (v4(host)) return .{ .kind = .ipv4, .value = host };
    if (v6(host)) return .{ .kind = .ipv6, .value = host };
    var norm = host;
    while (norm.len > 0 and norm[norm.len - 1] == '.') norm = norm[0 .. norm.len - 1]; // trailing dot(s)
    if (std.mem.indexOfScalar(u8, norm, '.') == null) return .{}; // need a dot
    if (std.mem.indexOf(u8, norm, "..") != null) return .{}; // no empty label
    var it = std.mem.splitScalar(u8, norm, '.');
    var last: []const u8 = "";
    while (it.next()) |label| {
        if (!labelOk(label)) return .{};
        last = label;
    }
    return .{ .kind = .domain, .value = norm, .tld = last };
}
fn objectPath(k: Kind) ?[]const u8 { // the RDAP object path segment
    return switch (k) {
        .domain => "domain",
        .ipv4, .ipv6 => "ip",
        .asn => "autnum",
        .invalid => null,
    };
}
fn buildRdapUrl(base: []const u8, c: Classified, out: []u8) []const u8 { // one slash before the path
    const path = objectPath(c.kind) orelse return out[0..0];
    var b = base;
    while (b.len > 0 and b[b.len - 1] == '/') b = b[0 .. b.len - 1];
    return std.fmt.bufPrint(out, "{s}/{s}/{s}", .{ b, path, c.value }) catch out[0..0];
}
fn asnInRange(key: []const u8, asn: u64) bool { // asn.json keys: "lo-hi" or bare
    if (std.mem.indexOfScalar(u8, key, '-')) |dash| {
        const lo = std.fmt.parseInt(u64, key[0..dash], 10) catch return false;
        const hi = std.fmt.parseInt(u64, key[dash + 1 ..], 10) catch return false;
        return asn >= lo and asn <= hi;
    }
    return (std.fmt.parseInt(u64, key, 10) catch 0) == asn;
}
pub fn main() !void { // bootstrap excerpts (dns.json / asn.json) -> authoritative RDAP base
    const stdout = std.io.getStdOut().writer();
    const queries = [_][]const u8{ "https://CosmoLabs.org/tools#whois", "AS3356", "192.0.2.1", "not_a_domain" };
    for (queries) |q| {
        var buf: [512]u8 = undefined;
        const c = classify(q, &buf);
        var base: ?[]const u8 = null;
        if (c.kind == .domain and std.mem.eql(u8, c.tld, "org")) {
            base = "https://rdap.pir.org/rdap/";
        } else if (c.kind == .asn) {
            const asn = std.fmt.parseInt(u64, c.value, 10) catch 0;
            if (asnInRange("64496-64511", asn)) base = "https://rdap.arin.net/registry";
        }
        var url_buf: [512]u8 = undefined;
        const url = if (base) |b| buildRdapUrl(b, c, &url_buf) else "";
        try stdout.print("{s: <34} {s: <8} {s}\n", .{ q, objectPath(c.kind) orelse "invalid", url });
    }
}

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 →