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 →